diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 657537f6d4..b890dc326e 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -217,6 +217,7 @@ Read subagents on-demand. Full index: `subagent-index.toon`. | Bundles | `bundles/*.json`, `scripts/bundle-helper.sh`, `tools/context/model-routing.md` | | Model routing | `tools/context/model-routing.md`, `reference/orchestration.md` | | Orchestration | `reference/orchestration.md`, `tools/ai-assistants/headless-dispatch.md`, `scripts/commands/pulse.md`, `scripts/commands/dashboard.md` | +| Upstream watch | `scripts/upstream-watch-helper.sh`, `.agents/configs/upstream-watch.json` | | Agent/MCP dev | `tools/build-agent/build-agent.md`, `tools/build-mcp/build-mcp.md`, `tools/mcp-toolkit/mcporter.md` | | Framework | `aidevops/architecture.md`, `scripts/commands/skills.md` | @@ -231,8 +232,9 @@ Key capabilities (details in `reference/orchestration.md`, `reference/services.m - **Memory**: cross-session SQLite FTS5 (`/remember`, `/recall`) - **Orchestration**: supervisor dispatch, pulse scheduler, auto-pickup, cross-repo issue/PR/TODO visibility - **Contribution watch**: monitors external issues/PRs for new comments needing reply. `contribution-watch-helper.sh seed|scan|status|install|uninstall`. Prompt-injection-safe — automated scans are deterministic (no LLM), comment bodies only shown in interactive sessions after `prompt-guard-helper.sh scan`. +- **Upstream watch**: monitors external repos we've borrowed ideas/code from for new releases. `upstream-watch-helper.sh add|remove|check|ack|status`. Shows release diffs and changelogs between our last-seen version and latest. Distinct from skill imports (code we pulled in) and contribution watch (repos we filed issues on) — this tracks "inspiration repos" for passive monitoring. Config: `.agents/configs/upstream-watch.json`. - **Skills**: `aidevops skills`, `/skills` -- **Auto-update**: GitHub poll + daily skill/repo sync +- **Auto-update**: GitHub poll + daily skill/upstream watch/OpenClaw/tool freshness checks (via `auto-update-helper.sh`). Repo sync runs separately via `aidevops repo-sync` scheduler. - **Browser**: Playwright, dev-browser (persistent login) - **Quality**: Write-time per-edit linting → `linters-local.sh` → `/pr review` → `/postflight`. Fix violations at edit time, not commit time. See `prompts/build.txt` "Write-Time Quality Enforcement". Bundle `skip_gates` filter irrelevant checks per project type. - **Sessions**: `/session-review`, `/checkpoint`, compaction resilience diff --git a/.agents/configs/upstream-watch.json b/.agents/configs/upstream-watch.json new file mode 100644 index 0000000000..6796d6fde1 --- /dev/null +++ b/.agents/configs/upstream-watch.json @@ -0,0 +1,4 @@ +{ + "$comment": "Upstream repos to watch for releases and significant changes. Managed by upstream-watch-helper.sh (t1426). Entry schema: {slug, description, relevance, default_branch, added_at}. State (last_release_seen, last_commit_seen, last_checked, updates_pending) stored in ~/.aidevops/cache/upstream-watch-state.json.", + "repos": [] +} diff --git a/.agents/reference/services.md b/.agents/reference/services.md index caf1d80b3a..04ba267ecf 100644 --- a/.agents/reference/services.md +++ b/.agents/reference/services.md @@ -128,6 +128,8 @@ Automatic polling for new releases. Checks GitHub every 10 minutes and runs `aid **Daily skill refresh**: Each auto-update check also runs a 24h-gated skill freshness check. If >24h have passed since the last check, `skill-update-helper.sh --auto-update --quiet` pulls upstream changes for all imported skills. State is tracked in `~/.aidevops/cache/auto-update-state.json` (`last_skill_check`, `skill_updates_applied`). Disable with `AIDEVOPS_SKILL_AUTO_UPDATE=false`; adjust frequency with `AIDEVOPS_SKILL_FRESHNESS_HOURS=` (default: 24). View skill check status with `aidevops auto-update status`. +**Upstream watch**: Alongside skill refresh, `upstream-watch-helper.sh check` monitors external repos we've borrowed ideas/code from for new releases. Unlike skill imports (code we pulled in) or contribution watch (repos we filed issues on), this tracks "inspiration repos" — repos we want to passively monitor for improvements relevant to our implementation. Config: `.agents/configs/upstream-watch.json`. State: `~/.aidevops/cache/upstream-watch-state.json`. Run `upstream-watch-helper.sh status` to see watched repos, `upstream-watch-helper.sh check` to check for updates, `upstream-watch-helper.sh ack ` after reviewing. + **Repo version wins on update**: When `aidevops update` runs, shared agents in `~/.aidevops/agents/` are overwritten by the repo version. Only `custom/` and `draft/` directories are preserved. Imported skills stored outside these directories will be overwritten. To keep a skill across updates, either re-import it after each update or move it to `custom/`. ## Repo Sync diff --git a/.agents/reference/settings.md b/.agents/reference/settings.md index db63558d8f..3906432620 100644 --- a/.agents/reference/settings.md +++ b/.agents/reference/settings.md @@ -35,6 +35,8 @@ Controls automatic update behavior for aidevops, skills, tools, and OpenClaw. | `auto_update.tool_idle_hours` | number | `6` | `AIDEVOPS_TOOL_IDLE_HOURS` | Required user idle time (hours) before tool updates run. Prevents updates during active work. | | `auto_update.openclaw_auto_update` | boolean | `true` | `AIDEVOPS_OPENCLAW_AUTO_UPDATE` | Enable daily OpenClaw update checks (only if openclaw CLI is installed). | | `auto_update.openclaw_freshness_hours` | number | `24` | `AIDEVOPS_OPENCLAW_FRESHNESS_HOURS` | Hours between OpenClaw update checks. | +| `auto_update.upstream_watch` | boolean | `true` | `AIDEVOPS_UPSTREAM_WATCH` | Enable daily upstream repo watch checks. Monitors external repos for new releases. | +| `auto_update.upstream_watch_hours` | number | `24` | `AIDEVOPS_UPSTREAM_WATCH_HOURS` | Hours between upstream watch checks. | ### supervisor diff --git a/.agents/scripts/auto-update-helper.sh b/.agents/scripts/auto-update-helper.sh index 934200f019..2b4595f355 100755 --- a/.agents/scripts/auto-update-helper.sh +++ b/.agents/scripts/auto-update-helper.sh @@ -35,6 +35,8 @@ # AIDEVOPS_TOOL_AUTO_UPDATE=false Disable 6-hourly tool freshness check # AIDEVOPS_TOOL_FRESHNESS_HOURS=6 Hours between tool checks (default: 6) # AIDEVOPS_TOOL_IDLE_HOURS=6 Required user idle hours before tool updates (default: 6) +# AIDEVOPS_UPSTREAM_WATCH=false Disable daily upstream repo watch check +# AIDEVOPS_UPSTREAM_WATCH_HOURS=24 Hours between upstream watch checks (default: 24) # # Logs: ~/.aidevops/logs/auto-update.log @@ -71,6 +73,7 @@ readonly DEFAULT_SKILL_FRESHNESS_HOURS=24 readonly DEFAULT_OPENCLAW_FRESHNESS_HOURS=24 readonly DEFAULT_TOOL_FRESHNESS_HOURS=6 readonly DEFAULT_TOOL_IDLE_HOURS=6 +readonly DEFAULT_UPSTREAM_WATCH_HOURS=24 readonly LAUNCHD_LABEL="com.aidevops.aidevops-auto-update" readonly LAUNCHD_DIR="$HOME/Library/LaunchAgents" readonly LAUNCHD_PLIST="${LAUNCHD_DIR}/${LAUNCHD_LABEL}.plist" @@ -859,6 +862,120 @@ update_tool_check_timestamp() { return 0 } +####################################### +# Check upstream-watched repos for new releases (24h gate) +# Called from cmd_check after tool freshness check. +# Respects config: aidevops config set updates.upstream_watch false +####################################### +check_upstream_watch() { + # Opt-out via config (env var or config file) + if ! is_feature_enabled upstream_watch; then + log_info "Upstream watch disabled via config" + return 0 + fi + + local freshness_hours + freshness_hours=$(get_feature_toggle upstream_watch_hours "$DEFAULT_UPSTREAM_WATCH_HOURS") + if ! [[ "$freshness_hours" =~ ^[0-9]+$ ]] || [[ "$freshness_hours" -eq 0 ]]; then + log_warn "updates.upstream_watch_hours='${freshness_hours}' is not a positive integer — using default (${DEFAULT_UPSTREAM_WATCH_HOURS}h)" + freshness_hours="$DEFAULT_UPSTREAM_WATCH_HOURS" + fi + local freshness_seconds=$((freshness_hours * 3600)) + + # Read last upstream watch check timestamp from state file + local last_upstream_check="" + if [[ -f "$STATE_FILE" ]] && command -v jq &>/dev/null; then + last_upstream_check=$(jq -r '.last_upstream_watch_check // empty' "$STATE_FILE" 2>/dev/null || true) + fi + + # Determine if check is needed + local needs_check=true + if [[ -n "$last_upstream_check" ]]; then + local last_epoch now_epoch elapsed + if [[ "$(uname)" == "Darwin" ]]; then + last_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_upstream_check" "+%s" 2>/dev/null || echo "0") + else + last_epoch=$(date -d "$last_upstream_check" "+%s" 2>/dev/null || echo "0") + fi + now_epoch=$(date +%s) + elapsed=$((now_epoch - last_epoch)) + + if [[ $elapsed -lt $freshness_seconds ]]; then + log_info "Upstream watch checked ${elapsed}s ago (gate: ${freshness_seconds}s) — skipping" + needs_check=false + fi + fi + + if [[ "$needs_check" != "true" ]]; then + return 0 + fi + + # Locate upstream-watch-helper.sh (respect AIDEVOPS_AGENTS_DIR) + local agents_dir="${AIDEVOPS_AGENTS_DIR:-$HOME/.aidevops/agents}" + local upstream_watch_script="${agents_dir}/scripts/upstream-watch-helper.sh" + if [[ ! -x "$upstream_watch_script" ]]; then + upstream_watch_script="$INSTALL_DIR/.agents/scripts/upstream-watch-helper.sh" + fi + + if [[ ! -x "$upstream_watch_script" ]]; then + log_info "upstream-watch-helper.sh not found — skipping upstream watch check" + return 0 + fi + + # Check if upstream-watch.json has any repos + local watch_config="${agents_dir}/configs/upstream-watch.json" + if [[ ! -f "$watch_config" ]]; then + log_info "No upstream watch config found — skipping" + update_upstream_watch_timestamp + return 0 + fi + + local repo_count + repo_count=$(jq '.repos | length' "$watch_config" 2>/dev/null || echo "0") + if [[ "$repo_count" -eq 0 ]]; then + log_info "No repos in upstream watchlist — skipping" + update_upstream_watch_timestamp + return 0 + fi + + log_info "Running daily upstream watch check (${repo_count} repos)..." + if "$upstream_watch_script" check >>"$LOG_FILE" 2>&1; then + log_info "Upstream watch check complete" + update_upstream_watch_timestamp + else + log_warn "Upstream watch check had errors (exit code: $?) — will retry next run" + fi + return 0 +} + +####################################### +# Record last_upstream_watch_check timestamp in state file +####################################### +update_upstream_watch_timestamp() { + local timestamp + timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + if command -v jq &>/dev/null; then + local tmp_state + tmp_state=$(mktemp) + trap 'rm -f "${tmp_state:-}"' RETURN + + if [[ -f "$STATE_FILE" ]]; then + if ! jq --arg ts "$timestamp" \ + '. + {last_upstream_watch_check: $ts}' \ + "$STATE_FILE" >"$tmp_state" 2>&1; then + log_warn "Failed to update upstream watch timestamp (jq error on state file)" + return 1 + fi + mv "$tmp_state" "$STATE_FILE" + else + jq -n --arg ts "$timestamp" \ + '{last_upstream_watch_check: $ts}' >"$STATE_FILE" + fi + fi + return 0 +} + ####################################### # One-shot check and update # This is what the cron job calls @@ -897,6 +1014,7 @@ cmd_check() { check_skill_freshness check_openclaw_freshness check_tool_freshness + check_upstream_watch return 0 fi @@ -927,6 +1045,7 @@ cmd_check() { check_skill_freshness check_openclaw_freshness check_tool_freshness + check_upstream_watch return 0 fi @@ -941,6 +1060,7 @@ cmd_check() { check_skill_freshness check_openclaw_freshness check_tool_freshness + check_upstream_watch return 1 fi @@ -951,6 +1071,7 @@ cmd_check() { check_skill_freshness check_openclaw_freshness check_tool_freshness + check_upstream_watch return 1 fi @@ -960,6 +1081,7 @@ cmd_check() { check_skill_freshness check_openclaw_freshness check_tool_freshness + check_upstream_watch return 1 fi @@ -986,6 +1108,7 @@ cmd_check() { check_skill_freshness check_openclaw_freshness check_tool_freshness + check_upstream_watch return 1 fi @@ -998,6 +1121,9 @@ cmd_check() { # Run 6-hourly tool freshness check (idle-gated) check_tool_freshness + # Run daily upstream watch check (24h gate) + check_upstream_watch + return 0 } diff --git a/.agents/scripts/shared-constants.sh b/.agents/scripts/shared-constants.sh index f0472b88f7..2eb3df3afb 100755 --- a/.agents/scripts/shared-constants.sh +++ b/.agents/scripts/shared-constants.sh @@ -1446,6 +1446,8 @@ _ft_env_map() { repo_sync) echo "AIDEVOPS_REPO_SYNC" ;; openclaw_auto_update) echo "AIDEVOPS_OPENCLAW_AUTO_UPDATE" ;; openclaw_freshness_hours) echo "AIDEVOPS_OPENCLAW_FRESHNESS_HOURS" ;; + upstream_watch) echo "AIDEVOPS_UPSTREAM_WATCH" ;; + upstream_watch_hours) echo "AIDEVOPS_UPSTREAM_WATCH_HOURS" ;; max_interactive_sessions) echo "AIDEVOPS_MAX_SESSIONS" ;; *) echo "" ;; esac @@ -1478,7 +1480,7 @@ _load_feature_toggles_legacy() { done <"$FEATURE_TOGGLES_USER" fi - local toggle_keys="auto_update update_interval skill_auto_update skill_freshness_hours tool_auto_update tool_freshness_hours tool_idle_hours supervisor_pulse repo_sync openclaw_auto_update openclaw_freshness_hours manage_opencode_config manage_claude_config session_greeting safety_hooks shell_aliases onboarding_prompt" + local toggle_keys="auto_update update_interval skill_auto_update skill_freshness_hours tool_auto_update tool_freshness_hours tool_idle_hours supervisor_pulse repo_sync openclaw_auto_update openclaw_freshness_hours upstream_watch upstream_watch_hours max_interactive_sessions manage_opencode_config manage_claude_config session_greeting safety_hooks shell_aliases onboarding_prompt" local tk env_var env_val for tk in $toggle_keys; do env_var=$(_ft_env_map "$tk") diff --git a/.agents/scripts/upstream-watch-helper.sh b/.agents/scripts/upstream-watch-helper.sh new file mode 100755 index 0000000000..54b0d99c99 --- /dev/null +++ b/.agents/scripts/upstream-watch-helper.sh @@ -0,0 +1,912 @@ +#!/usr/bin/env bash +# upstream-watch-helper.sh — Track external repos for release monitoring (t1426) +# +# Maintains a watchlist of external repos we've borrowed ideas/code from. +# Checks for new releases and significant commits, shows changelog diffs +# between our last-seen version and latest. Distinct from: +# - skill-sources.json (imported skills — tracked by add-skill-helper.sh) +# - contribution-watch (repos we've contributed to) +# +# This covers "inspiration repos" — repos we want to passively monitor +# for improvements relevant to our implementation. +# +# Usage: +# upstream-watch-helper.sh add [--relevance "why we care"] +# upstream-watch-helper.sh remove +# upstream-watch-helper.sh check [--verbose] Check all watched repos for updates +# upstream-watch-helper.sh check Check a specific repo +# upstream-watch-helper.sh ack Acknowledge latest release (mark as seen) +# upstream-watch-helper.sh status Show all watched repos and their state +# upstream-watch-helper.sh help Show usage +# +# Config: ~/.aidevops/agents/configs/upstream-watch.json (template committed) +# State: ~/.aidevops/cache/upstream-watch-state.json (runtime, gitignored) +# Log: ~/.aidevops/logs/upstream-watch.log + +set -euo pipefail + +# PATH normalisation for launchd/MCP environments +export PATH="/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin:${PATH}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 1 + +# shellcheck source=shared-constants.sh +source "${SCRIPT_DIR}/shared-constants.sh" 2>/dev/null || true + +# Fallback colours if shared-constants.sh not loaded +[[ -z "${RED+x}" ]] && RED='\033[0;31m' +[[ -z "${GREEN+x}" ]] && GREEN='\033[0;32m' +[[ -z "${YELLOW+x}" ]] && YELLOW='\033[1;33m' +[[ -z "${BLUE+x}" ]] && BLUE='\033[0;34m' +[[ -z "${CYAN+x}" ]] && CYAN='\033[0;36m' +[[ -z "${NC+x}" ]] && NC='\033[0m' + +# ============================================================================= +# Configuration +# ============================================================================= + +AGENTS_DIR="${AIDEVOPS_AGENTS_DIR:-$HOME/.aidevops/agents}" +CONFIG_FILE="${AGENTS_DIR}/configs/upstream-watch.json" +STATE_FILE="${HOME}/.aidevops/cache/upstream-watch-state.json" +LOGFILE="${HOME}/.aidevops/logs/upstream-watch.log" + +# Logging prefix for shared log_* functions +# shellcheck disable=SC2034 +LOG_PREFIX="upstream-watch" + +# ============================================================================= +# Logging (standalone — shared-constants.sh log_* may not be available) +# ============================================================================= + +####################################### +# Write a timestamped log entry to the upstream-watch log file +# Arguments: +# $1 - Log level (INFO, WARN, ERROR) +# $@ - Log message +####################################### +_log() { + local level="$1" + shift + local msg="$*" + local timestamp + timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ) + local log_dir + log_dir=$(dirname "$LOGFILE") + mkdir -p "$log_dir" 2>/dev/null || true + echo "[${timestamp}] [${level}] ${msg}" >>"$LOGFILE" + return 0 +} + +####################################### +# Log an informational message +####################################### +_log_info() { + _log "INFO" "$@" + return 0 +} + +####################################### +# Log a warning message +####################################### +_log_warn() { + _log "WARN" "$@" + return 0 +} + +####################################### +# Log an error message +####################################### +_log_error() { + _log "ERROR" "$@" + return 0 +} + +# ============================================================================= +# Prerequisites +# ============================================================================= + +####################################### +# Verify required tools (gh, jq) are installed and gh is authenticated +# Returns: 0 if all prerequisites met, 1 otherwise +####################################### +_check_prerequisites() { + if ! command -v gh &>/dev/null; then + echo -e "${RED}Error: gh CLI not found. Install from https://cli.github.com/${NC}" >&2 + return 1 + fi + if ! command -v jq &>/dev/null; then + echo -e "${RED}Error: jq not found. Install with: brew install jq${NC}" >&2 + return 1 + fi + if ! gh auth status &>/dev/null; then + echo -e "${RED}Error: gh not authenticated. Run: gh auth login${NC}" >&2 + return 1 + fi + return 0 +} + +# ============================================================================= +# State file management +# ============================================================================= + +####################################### +# Create the state file with empty defaults if it doesn't exist +####################################### +_ensure_state_file() { + local state_dir + state_dir=$(dirname "$STATE_FILE") + mkdir -p "$state_dir" 2>/dev/null || true + + if [[ ! -f "$STATE_FILE" ]]; then + echo '{"last_check":"","repos":{}}' >"$STATE_FILE" + _log_info "Created new state file: $STATE_FILE" + fi + return 0 +} + +####################################### +# Read and output the current state JSON +####################################### +_read_state() { + _ensure_state_file + cat "$STATE_FILE" + return 0 +} + +####################################### +# Write state JSON to the state file, validating JSON first +# Arguments: +# $1 - JSON string to write +####################################### +_write_state() { + local state="$1" + _ensure_state_file + local jq_err + jq_err=$(echo "$state" | jq '.' 2>&1 >"$STATE_FILE") || { + _log_error "Failed to write state file (invalid JSON): ${jq_err}" + return 1 + } + return 0 +} + +# ============================================================================= +# Config file management +# ============================================================================= + +####################################### +# Create the config file with empty defaults if it doesn't exist +####################################### +_ensure_config_file() { + local config_dir + config_dir=$(dirname "$CONFIG_FILE") + mkdir -p "$config_dir" 2>/dev/null || true + + if [[ ! -f "$CONFIG_FILE" ]]; then + cat >"$CONFIG_FILE" <<'DEFAULTCONFIG' +{ + "$comment": "Upstream repos to watch for releases and significant changes. Managed by upstream-watch-helper.sh.", + "repos": [] +} +DEFAULTCONFIG + _log_info "Created new config file: $CONFIG_FILE" + fi + return 0 +} + +####################################### +# Read and output the current config JSON +####################################### +_read_config() { + _ensure_config_file + cat "$CONFIG_FILE" + return 0 +} + +####################################### +# Write config JSON to the config file, validating JSON first +# Arguments: +# $1 - JSON string to write +####################################### +_write_config() { + local config="$1" + _ensure_config_file + local jq_err + jq_err=$(echo "$config" | jq '.' 2>&1 >"$CONFIG_FILE") || { + _log_error "Failed to write config file (invalid JSON): ${jq_err}" + return 1 + } + return 0 +} + +# ============================================================================= +# ISO 8601 helpers +# ============================================================================= + +####################################### +# Output the current UTC time in ISO 8601 format +####################################### +_now_iso() { + date -u +%Y-%m-%dT%H:%M:%SZ + return 0 +} + +# ============================================================================= +# Commands +# ============================================================================= + +####################################### +# Add a repository to the upstream watchlist +# Verifies the repo exists, captures initial state (latest release/commit), +# and stores config + state so the first check doesn't flag everything as new. +# Arguments: +# $1 - Repository slug (owner/repo) +# $2 - Optional relevance description +####################################### +cmd_add() { + local slug="$1" + local relevance="${2:-}" + + if [[ -z "$slug" ]]; then + echo -e "${RED}Error: Repository slug required (owner/repo)${NC}" >&2 + return 1 + fi + + # Validate slug format + if [[ ! "$slug" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then + echo -e "${RED}Error: Invalid slug format. Expected: owner/repo${NC}" >&2 + return 1 + fi + + _check_prerequisites || return 1 + + # Check if already watched + local config + config=$(_read_config) + local existing + existing=$(echo "$config" | jq -r --arg slug "$slug" '.repos[] | select(.slug == $slug) | .slug') + if [[ -n "$existing" ]]; then + echo -e "${YELLOW}Already watching: ${slug}${NC}" + return 0 + fi + + # Verify repo exists and get metadata + echo -e "${BLUE}Verifying repo: ${slug}...${NC}" + local repo_info repo_err + repo_err=$(gh api "repos/${slug}" --jq '{description, stargazers_count, pushed_at, default_branch}' 2>&1) && repo_info="$repo_err" || { + echo -e "${RED}Error: Could not access repo ${slug}: ${repo_err}${NC}" >&2 + return 1 + } + + local description default_branch + description=$(echo "$repo_info" | jq -r '.description // "No description"') + default_branch=$(echo "$repo_info" | jq -r '.default_branch // "main"') + + # Get latest release (if any) + local latest_release latest_tag + latest_release=$(gh api "repos/${slug}/releases/latest" --jq '{tag_name, published_at, name}' 2>/dev/null) || latest_release="" + if [[ -n "$latest_release" ]]; then + latest_tag=$(echo "$latest_release" | jq -r '.tag_name') + else + latest_tag="" + fi + + # Get latest commit SHA + local latest_commit + latest_commit=$(gh api "repos/${slug}/commits?per_page=1" --jq '.[0].sha // empty' 2>/dev/null) || latest_commit="" + + # Build the entry + local now + now=$(_now_iso) + local new_entry + new_entry=$(jq -n \ + --arg slug "$slug" \ + --arg desc "$description" \ + --arg relevance "$relevance" \ + --arg branch "$default_branch" \ + --arg added "$now" \ + '{ + slug: $slug, + description: $desc, + relevance: $relevance, + default_branch: $branch, + added_at: $added + }') + + # Add to config + config=$(echo "$config" | jq --argjson entry "$new_entry" '.repos += [$entry]') + _write_config "$config" + + # Set initial state (so first check doesn't flag everything as new) + local state + state=$(_read_state) + local state_entry + state_entry=$(jq -n \ + --arg tag "$latest_tag" \ + --arg commit "${latest_commit:0:7}" \ + --arg checked "$now" \ + '{ + last_release_seen: $tag, + last_commit_seen: $commit, + last_checked: $checked, + updates_pending: 0 + }') + state=$(echo "$state" | jq --arg slug "$slug" --argjson entry "$state_entry" '.repos[$slug] = $entry') + _write_state "$state" + + echo -e "${GREEN}Now watching: ${slug}${NC}" + echo " Description: ${description}" + [[ -n "$relevance" ]] && echo " Relevance: ${relevance}" + [[ -n "$latest_tag" ]] && echo " Latest release: ${latest_tag}" + echo " Default branch: ${default_branch}" + _log_info "Added watch: ${slug} (relevance: ${relevance:-none})" + return 0 +} + +####################################### +# Remove a repository from the upstream watchlist and clean up its state +# Arguments: +# $1 - Repository slug (owner/repo) +####################################### +cmd_remove() { + local slug="$1" + + if [[ -z "$slug" ]]; then + echo -e "${RED}Error: Repository slug required (owner/repo)${NC}" >&2 + return 1 + fi + + local config + config=$(_read_config) + local existing + existing=$(echo "$config" | jq -r --arg slug "$slug" '.repos[] | select(.slug == $slug) | .slug') + if [[ -z "$existing" ]]; then + echo -e "${YELLOW}Not watching: ${slug}${NC}" + return 0 + fi + + config=$(echo "$config" | jq --arg slug "$slug" '.repos = [.repos[] | select(.slug != $slug)]') + _write_config "$config" + + # Remove from state + local state + state=$(_read_state) + state=$(echo "$state" | jq --arg slug "$slug" 'del(.repos[$slug])') + _write_state "$state" + + echo -e "${GREEN}Removed: ${slug}${NC}" + _log_info "Removed watch: ${slug}" + return 0 +} + +####################################### +# Check watched repos for new releases and commits +# Compares current GitHub state against last-seen state. Reports new +# releases with changelog diffs and new commits. Does NOT advance +# last_seen — that requires explicit ack. Returns 1 if any probe failed. +# Arguments: +# $1 - Optional target slug to check a single repo +# Globals: +# VERBOSE - Show commit-level detail when true +####################################### +cmd_check() { + local target_slug="${1:-}" + local verbose="${VERBOSE:-false}" + + _check_prerequisites || return 1 + + local config + config=$(_read_config) + local state + state=$(_read_state) + + local slugs + if [[ -n "$target_slug" ]]; then + # Validate that the target slug is on the watchlist + if ! echo "$config" | jq -e --arg slug "$target_slug" '.repos[] | select(.slug == $slug)' >/dev/null 2>&1; then + echo -e "${RED}Error: Not watching ${target_slug}. Add it first with 'upstream-watch-helper.sh add ${target_slug}'.${NC}" >&2 + return 1 + fi + slugs="$target_slug" + else + slugs=$(echo "$config" | jq -r '.repos[].slug') + fi + + if [[ -z "$slugs" ]]; then + echo -e "${BLUE}No repos being watched. Use 'add' to start watching.${NC}" + return 0 + fi + + local updates_found=0 + local had_probe_failure=false + local now + now=$(_now_iso) + + while IFS= read -r slug; do + [[ -z "$slug" ]] && continue + + # Get relevance from config + local relevance + relevance=$(echo "$config" | jq -r --arg slug "$slug" '.repos[] | select(.slug == $slug) | .relevance // ""') + + # Get last-seen state + local last_release_seen last_commit_seen + last_release_seen=$(echo "$state" | jq -r --arg slug "$slug" '.repos[$slug].last_release_seen // ""') + last_commit_seen=$(echo "$state" | jq -r --arg slug "$slug" '.repos[$slug].last_commit_seen // ""') + + # --- Check releases --- + local latest_release_tag="" latest_release_name="" latest_release_date="" + local release_json="" + local probe_failed=false + local api_stderr + api_stderr=$(mktemp) + if release_json=$(gh api "repos/${slug}/releases/latest" 2>"$api_stderr"); then + : # success — release_json has the response + else + local release_err + release_err=$(<"$api_stderr") + # 404 = no releases (normal), anything else = real error + if [[ "$release_err" == *"Not Found"* || "$release_err" == *"404"* ]]; then + release_json="" + else + _log_warn "gh api releases failed for ${slug}: ${release_err}" + echo -e "${YELLOW}Warning${NC}: Could not fetch releases for ${slug}" >&2 + release_json="" + probe_failed=true + fi + fi + rm -f "$api_stderr" + + if [[ -n "$release_json" ]]; then + latest_release_tag=$(echo "$release_json" | jq -r '.tag_name // ""') + latest_release_name=$(echo "$release_json" | jq -r '.name // ""') + latest_release_date=$(echo "$release_json" | jq -r '.published_at // ""') + fi + + local has_new_release=false + if [[ -n "$latest_release_tag" && "$latest_release_tag" != "$last_release_seen" ]]; then + has_new_release=true + fi + + # --- Check commits (even if no new release) --- + local latest_commit="" latest_commit_date="" + local commit_json="" + api_stderr=$(mktemp) + if commit_json=$(gh api "repos/${slug}/commits?per_page=1" --jq '.[0]' 2>"$api_stderr"); then + : # success + else + local commit_err + commit_err=$(<"$api_stderr") + _log_warn "gh api commits failed for ${slug}: ${commit_err}" + echo -e "${YELLOW}Warning${NC}: Could not fetch commits for ${slug}" >&2 + commit_json="" + probe_failed=true + fi + rm -f "$api_stderr" + + if [[ -n "$commit_json" ]]; then + latest_commit=$(echo "$commit_json" | jq -r '.sha // ""') + latest_commit_date=$(echo "$commit_json" | jq -r '.commit.committer.date // ""') + fi + + local has_new_commits=false + if [[ -n "$latest_commit" && "${latest_commit:0:7}" != "$last_commit_seen" ]]; then + has_new_commits=true + fi + + # --- Report --- + if [[ "$has_new_release" == true ]]; then + updates_found=$((updates_found + 1)) + echo "" + echo -e "${YELLOW}NEW RELEASE${NC}: ${slug}" + [[ -n "$relevance" ]] && echo -e " Relevance: ${CYAN}${relevance}${NC}" + echo " Previous: ${last_release_seen:-none}" + echo " Latest: ${latest_release_tag} (${latest_release_date:-unknown})" + [[ -n "$latest_release_name" && "$latest_release_name" != "$latest_release_tag" ]] && + echo " Name: ${latest_release_name}" + + # Show releases between last-seen and latest + _show_release_diff "$slug" "$last_release_seen" "$latest_release_tag" + + # Show commit summary if verbose + if [[ "$verbose" == true ]]; then + _show_commit_diff "$slug" "$last_commit_seen" "${latest_commit:0:7}" + fi + + echo " Action: Review changes, then run: upstream-watch-helper.sh ack ${slug}" + + elif [[ "$has_new_commits" == true ]]; then + updates_found=$((updates_found + 1)) + echo "" + echo -e "${BLUE}NEW COMMITS${NC}: ${slug} (no new release)" + [[ -n "$relevance" ]] && echo -e " Relevance: ${CYAN}${relevance}${NC}" + if [[ "$verbose" == true ]]; then + _show_commit_diff "$slug" "$last_commit_seen" "${latest_commit:0:7}" + else + echo " Latest commit: ${latest_commit:0:7} (${latest_commit_date:-unknown})" + echo " Action: Review changes, then run: upstream-watch-helper.sh ack ${slug}" + fi + else + echo -e "${GREEN}Up to date${NC}: ${slug} (${latest_release_tag:-no releases})" + fi + + # Update last_checked and updates_pending (but NOT last_release_seen or last_commit_seen — those require explicit ack) + # Skip state update if probes failed to avoid masking errors as "up to date" + if [[ "$probe_failed" != true ]]; then + state=$(echo "$state" | jq --arg slug "$slug" --arg now "$now" \ + --argjson pending "$([[ "$has_new_release" == true || "$has_new_commits" == true ]] && echo 1 || echo 0)" \ + '.repos[$slug].last_checked = $now | .repos[$slug].updates_pending = $pending') + else + had_probe_failure=true + fi + + done <<<"$slugs" + + # Only advance global last_check if all probes succeeded — partial failures + # should not advance the 24h gate so the caller retries on the next cycle + if [[ "$had_probe_failure" != true ]]; then + state=$(echo "$state" | jq --arg now "$now" '.last_check = $now') + fi + _write_state "$state" + + echo "" + if [[ "$updates_found" -gt 0 ]]; then + echo -e "${YELLOW}${updates_found} repo(s) have updates to review.${NC}" + else + echo -e "${GREEN}All watched repos are up to date.${NC}" + fi + + _log_info "Check complete: ${updates_found} updates found" + [[ "$had_probe_failure" == true ]] && return 1 + return 0 +} + +####################################### +# Display release changelog between two tags +# Shows all releases between from_tag and to_tag, plus latest release notes. +# Arguments: +# $1 - Repository slug (owner/repo) +# $2 - From tag (last seen, empty for first check) +# $3 - To tag (latest release) +####################################### +_show_release_diff() { + local slug="$1" + local from_tag="$2" + local to_tag="$3" + + if [[ -z "$from_tag" ]]; then + # First time — just show the latest release notes + echo " Release notes:" + local body + body=$(gh api "repos/${slug}/releases/latest" --jq '.body // "No release notes"' 2>/dev/null) || body="Could not fetch" + sed -n '1,20{s/^/ /;p;}' <<<"$body" + local line_count + line_count=$(wc -l <<<"$body" | tr -d ' ') + if [[ "$line_count" -gt 20 ]]; then + echo " ... (${line_count} lines total — view full notes on GitHub)" + fi + return 0 + fi + + # Show all releases between from_tag and to_tag + local releases + releases=$(gh api --paginate "repos/${slug}/releases" --jq '.[].tag_name' 2>/dev/null) || { + echo " (Could not fetch release list)" + return 0 + } + + # Find releases newer than from_tag + local in_range=true + local release_count=0 + echo " Releases since ${from_tag}:" + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + if [[ "$tag" == "$from_tag" ]]; then + in_range=false + continue + fi + if [[ "$in_range" == true ]]; then + release_count=$((release_count + 1)) + # Get one-line summary for each release + local rel_name rel_date + rel_name=$(gh api "repos/${slug}/releases/tags/${tag}" --jq '.name // .tag_name' 2>/dev/null) || rel_name="$tag" + rel_date=$(gh api "repos/${slug}/releases/tags/${tag}" --jq '.published_at // ""' 2>/dev/null) || rel_date="" + local date_short="${rel_date:0:10}" + echo " ${tag} (${date_short}) — ${rel_name}" + fi + done <<<"$releases" + + if [[ "$release_count" -eq 0 ]]; then + echo " (none found — tags may not match release list)" + fi + + # Show latest release notes + echo "" + echo " Latest release notes (${to_tag}):" + local body + body=$(gh api "repos/${slug}/releases/tags/${to_tag}" --jq '.body // "No release notes"' 2>/dev/null) || body="Could not fetch" + sed -n '1,30{s/^/ /;p;}' <<<"$body" + local line_count + line_count=$(wc -l <<<"$body" | tr -d ' ') + if [[ "$line_count" -gt 30 ]]; then + echo " ... (${line_count} lines total)" + fi + return 0 +} + +####################################### +# Display recent commits between two SHAs +# Shows up to 10 commits newer than from_sha. +# Arguments: +# $1 - Repository slug (owner/repo) +# $2 - From SHA (7-char, last seen) +# $3 - To SHA (7-char, latest) +####################################### +_show_commit_diff() { + local slug="$1" + local from_sha="$2" + local to_sha="$3" + + if [[ -z "$from_sha" || "$from_sha" == "$to_sha" ]]; then + return 0 + fi + + echo " Recent commits:" + local commits + commits=$(gh api "repos/${slug}/commits?per_page=10" \ + --jq '.[] | "\(.sha[0:7]) \(.commit.message | split("\n")[0])"' 2>/dev/null) || { + echo " (Could not fetch commits)" + return 0 + } + + local count=0 + while IFS= read -r line; do + [[ -z "$line" ]] && continue + local sha="${line%% *}" + if [[ "$sha" == "$from_sha" ]]; then + break + fi + count=$((count + 1)) + echo " ${line}" + if [[ "$count" -ge 10 ]]; then + echo " ... (showing first 10)" + break + fi + done <<<"$commits" + + if [[ "$count" -eq 0 ]]; then + echo " (no new commits found in recent history)" + fi + return 0 +} + +####################################### +# Acknowledge the latest release/commit for a watched repo +# Updates last_release_seen and last_commit_seen to current, clears +# updates_pending. Validates slug against config watchlist first. +# Arguments: +# $1 - Repository slug (owner/repo) +####################################### +cmd_ack() { + local slug="$1" + + if [[ -z "$slug" ]]; then + echo -e "${RED}Error: Repository slug required (owner/repo)${NC}" >&2 + return 1 + fi + + _check_prerequisites || return 1 + + # Validate against config watchlist (consistent with cmd_check) + local config + config=$(_read_config) + if ! echo "$config" | jq -e --arg slug "$slug" '.repos[] | select(.slug == $slug)' >/dev/null 2>&1; then + echo -e "${RED}Error: Not watching ${slug}. Add it first with 'upstream-watch-helper.sh add ${slug}'.${NC}" >&2 + return 1 + fi + + local state + state=$(_read_state) + + # Get current latest release + local latest_tag + latest_tag=$(gh api "repos/${slug}/releases/latest" --jq '.tag_name' 2>/dev/null) || latest_tag="" + + local latest_commit + latest_commit=$(gh api "repos/${slug}/commits?per_page=1" --jq '.[0].sha // empty' 2>/dev/null) || latest_commit="" + + local now + now=$(_now_iso) + + state=$(echo "$state" | jq --arg slug "$slug" --arg tag "$latest_tag" \ + --arg commit "${latest_commit:0:7}" --arg now "$now" \ + '.repos[$slug].last_release_seen = $tag | .repos[$slug].last_commit_seen = $commit | .repos[$slug].last_checked = $now | .repos[$slug].updates_pending = 0') + _write_state "$state" + + echo -e "${GREEN}Acknowledged: ${slug} at ${latest_tag:-commit ${latest_commit:0:7}}${NC}" + _log_info "Acknowledged: ${slug} at ${latest_tag:-${latest_commit:0:7}}" + return 0 +} + +####################################### +# Display the status of all watched repos +# Shows repo count, last check time, and per-repo state including +# last release/commit seen, last checked date, and pending updates. +####################################### +cmd_status() { + local config + config=$(_read_config) + local state + state=$(_read_state) + + local repo_count + repo_count=$(echo "$config" | jq '.repos | length') + + if [[ "$repo_count" -eq 0 ]]; then + echo -e "${BLUE}No repos being watched.${NC}" + echo "" + echo "Add repos with: upstream-watch-helper.sh add --relevance \"why we care\"" + return 0 + fi + + local last_check + last_check=$(echo "$state" | jq -r '.last_check // "never"') + + echo -e "${BLUE}Upstream Watch Status${NC}" + echo "Repos watched: ${repo_count}" + echo "Last check: ${last_check}" + echo "" + + echo "$config" | jq -r '.repos[] | .slug' | while IFS= read -r slug; do + [[ -z "$slug" ]] && continue + + local relevance + relevance=$(echo "$config" | jq -r --arg slug "$slug" '.repos[] | select(.slug == $slug) | .relevance // ""') + local last_release last_commit last_checked pending + last_release=$(echo "$state" | jq -r --arg slug "$slug" '.repos[$slug].last_release_seen // "none"') + last_commit=$(echo "$state" | jq -r --arg slug "$slug" '.repos[$slug].last_commit_seen // "none"') + last_checked=$(echo "$state" | jq -r --arg slug "$slug" '.repos[$slug].last_checked // "never"') + pending=$(echo "$state" | jq -r --arg slug "$slug" '.repos[$slug].updates_pending // 0') + + if [[ "$pending" -gt 0 ]]; then + echo -e " ${YELLOW}*${NC} ${slug}" + else + echo -e " ${GREEN}-${NC} ${slug}" + fi + echo " Last release seen: ${last_release}" + echo " Last commit seen: ${last_commit}" + echo " Last checked: ${last_checked:0:10}" + [[ -n "$relevance" ]] && echo " Relevance: ${relevance}" + echo "" + done + + return 0 +} + +####################################### +# Display usage information and examples +####################################### +cmd_help() { + cat <<'EOF' +upstream-watch-helper.sh — Track external repos for release monitoring + +USAGE: + upstream-watch-helper.sh [options] + +COMMANDS: + add [--relevance "..."] Add a repo to the watchlist + remove Remove a repo from the watchlist + check [--verbose] Check all repos for new releases/commits + check Check a specific repo + ack Mark latest release as seen + status Show all watched repos and their state + help Show this help + +EXAMPLES: + # Watch a repo + upstream-watch-helper.sh add vercel-labs/portless \ + --relevance "Local dev hosting — compare against localdev-helper.sh" + + # Check for updates + upstream-watch-helper.sh check + upstream-watch-helper.sh check --verbose # Include commit-level detail + + # After reviewing, acknowledge the update + upstream-watch-helper.sh ack vercel-labs/portless + + # See what we're watching + upstream-watch-helper.sh status + +CONFIG: + Watchlist: ~/.aidevops/agents/configs/upstream-watch.json + State: ~/.aidevops/cache/upstream-watch-state.json + Log: ~/.aidevops/logs/upstream-watch.log + +INTEGRATION: + The pulse can call 'upstream-watch-helper.sh check' to surface + updates during supervisor sweeps. New releases appear as + informational items for human review. + + Skill imports (skill-sources.json) are tracked separately by + add-skill-helper.sh. This tool is for repos we haven't imported + from but want to monitor for ideas and improvements. +EOF + return 0 +} + +# ============================================================================= +# Main dispatch +# ============================================================================= + +####################################### +# Main entry point — parse command and dispatch to handler +# Arguments: +# $1 - Command (add, remove, check, ack, status, help) +# $@ - Command-specific arguments +####################################### +main() { + local cmd="${1:-help}" + shift || true + + case "$cmd" in + add) + local slug="" + local relevance="" + while [[ $# -gt 0 ]]; do + case "$1" in + --relevance) + if [[ $# -ge 2 && -n "${2:-}" && "${2:0:1}" != "-" ]]; then + relevance="$2" + shift 2 + else + echo -e "${RED}Error: --relevance requires a value${NC}" >&2 + return 1 + fi + ;; + *) + if [[ -z "$slug" ]]; then + slug="$1" + fi + shift + ;; + esac + done + cmd_add "$slug" "$relevance" + ;; + remove | rm) + cmd_remove "${1:-}" + ;; + check) + local target="" + local verbose=false + while [[ $# -gt 0 ]]; do + case "$1" in + --verbose | -v) + verbose=true + shift + ;; + *) + target="$1" + shift + ;; + esac + done + VERBOSE="$verbose" cmd_check "$target" + ;; + ack | acknowledge) + cmd_ack "${1:-}" + ;; + status | list) + cmd_status + ;; + help | --help | -h) + cmd_help + ;; + *) + echo -e "${RED}Unknown command: ${cmd}${NC}" >&2 + echo "Run 'upstream-watch-helper.sh help' for usage." >&2 + return 1 + ;; + esac +} + +main "$@" diff --git a/TODO.md b/TODO.md index 48ad46852b..0426c8d0f0 100644 --- a/TODO.md +++ b/TODO.md @@ -114,6 +114,8 @@ t1375,Prompt injection scanner — tool-agnostic defense for aidevops and agenti - [ ] t1428.4 Quarantine digest with learn feedback loop — create `quarantine-helper.sh` providing unified quarantine queue. `prompt-guard-helper.sh`, `network-tier-helper.sh`, and `sandbox-exec-helper.sh` write ambiguous-score items to `~/.aidevops/.agent-workspace/security/quarantine.jsonl`. `/security-review` command presents digest with approve/deny/learn actions. "Learn:allow" adds domain to `network-tiers-custom.conf` Tier 3. "Learn:deny" adds domain to Tier 5 or pattern to `prompt-guard-custom.txt`. "Learn:trust" adds MCP server to trusted list. Creates self-improving feedback loop — each review improves future scoring accuracy. Files: `.agents/scripts/quarantine-helper.sh` (new), `.agents/scripts/commands/security-review.md` (new), update `prompt-guard-helper.sh`, `network-tier-helper.sh`, `sandbox-exec-helper.sh` to call quarantine. #security #auto-dispatch ~5h model:sonnet blocked-by:t1428.3 - [ ] t1428.5 Unified post-session security summary — enhance `session-review-helper.sh` with `--security` mode aggregating: cost from `observability-helper.sh` (filtered to current session), security events from `audit-log-helper.sh`, flagged domains from `network-tier-helper.sh`, quarantine items pending review, prompt-guard detections from session. Single CLI summary table. All data sources exist — this is presentation/aggregation. Output format: tool call counts by type, security decisions (allowed/flagged/denied), cost breakdown by model, quality gate results. Files: `.agents/scripts/session-review-helper.sh`, `.agents/workflows/session-review.md` (update). #observability #auto-dispatch ~3h model:sonnet blocked-by:t1428.3,t1428.4 - [x] t1424 Zero-config localdev — auto-register projects and `localdev run` wrapper. Two gaps vs portless: (1) manual `localdev add` required before URLs work, (2) user must know and pass the assigned port to their dev command. Fix both: auto-register unregistered projects on first worktree creation or first `localdev run` (infer name from repo basename or package.json), and add `localdev run ` that sets PORT/HOST env vars and starts the process (e.g., `localdev run npm run dev` → sets PORT=3100, starts the command, `https://myapp.local` just works). For worktrees, `localdev run` in a worktree auto-creates the branch subdomain and uses the branch port. Subtasks: (1) auto-register in `localdev_auto_branch()` and new `localdev run`, (2) `localdev run` wrapper with PORT/HOST injection + process management, (3) update local-hosting.md docs. #feature #local-dev #auto-dispatch ~4h model:sonnet ref=GH#3998 logged:2026-03-09 pr:#4032 completed:2026-03-10 +- [ ] t1426 Upstream watch — track external repos for release monitoring. New `upstream-watch-helper.sh` (add/remove/check/ack/status) maintains a watchlist of external repos we've borrowed ideas/code from. Checks GitHub releases and commit activity, shows changelog diffs between last-seen and latest versions. Integrated with `auto-update-helper.sh` via 24h-gated `check_upstream_watch()`. Distinct from skill-sources.json (imported skills) and contribution-watch (repos we've contributed to) — this covers "inspiration repos" for passive monitoring. Config in `.agents/configs/upstream-watch.json`, state in `~/.aidevops/cache/upstream-watch-state.json`. First watched repo: vercel-labs/portless. #feature #monitoring ~3h model:sonnet ref:GH#3994 logged:2026-03-09 started:2026-03-09 +- [ ] t1427 Evaluate portless vs localdev-helper.sh for local dev hosting — portless (vercel-labs/portless) replaces port numbers with stable `.localhost` URLs, zero-dependency Node proxy, worktree-aware routing, agent-friendly design. Compare against our dnsmasq+Traefik+mkcert stack in `local-hosting.md`. Assess: adoption cost, feature parity, maintenance burden, whether to replace/complement/ignore. #evaluation #local-dev ~2h model:sonnet ref:GH#3995 logged:2026-03-09 blocked-by:t1426 - [x] t1423 Priority-class worker reservations for per-repo concurrency fairness — ensure high-priority repos get worker slots even when lower-priority repos have many queued tasks. #feature #orchestration #auto-dispatch ref:GH#3965 logged:2026-03-09 pr:#3966 completed:2026-03-09 - [x] t1420 Add `maintainer` field to repos.json for code-simplifier issue assignment — currently falls back to repo owner from slug, but a dedicated field allows assigning to the actual maintainer when owner != maintainer. Update repos.json schema, config-helper.sh, and code-simplifier docs. #chore ~1h ref=GH#3937 pr:#3942 completed:2026-03-09 - [x] t1414 Add Convos encrypted messaging agent — create `.agents/services/communications/convos.md` from upstream skill at `https://convos.org/skill.md`. Convos is an encrypted chat app built on XMTP with its own CLI (`@xmtp/convos-cli`). Covers: CLI install, join/create conversations, real-time agent participation via `convos agent serve` (ndjson protocol), bridge script template, group management, profiles, attachments, reactions. Add frontmatter + AI-CONTEXT markers, Related section linking to `xmtp.md`. Update `subagent-index.toon` and AGENTS.md domain index. Track upstream skill URL for future updates. #communications #agent #auto-dispatch ~2h model:sonnet ref:GH#3126 logged:2026-03-07 completed:2026-03-08 -> [todo/PLANS.md#2026-03-07-convos-agent]