spokesman

star 0

Thin user-interaction layer for the AgentMesh agentic workflow. Bootstraps the system (starts orchestrator.py daemon), then surfaces worker events to the user and relays decisions back to the orchestrator.

gara2000 By gara2000 schedule Updated 6/7/2026

name: spokesman description: Thin user-interaction layer for the AgentMesh agentic workflow. Bootstraps the system (starts orchestrator.py daemon), then surfaces worker events to the user and relays decisions back to the orchestrator. disable-model-invocation: true allowed-tools: Bash(notecove *, tmux *, mkdir *, cat *, echo *, rm *, bash *, sleep *, sed *, python3 *) hint: "Run the AgentMesh Spokesman (user-interaction layer). Required: --project . Optional: --profile , --max-workers (default 10), --mode (standard|auto-review, default standard), --review-limit (default 3), --no-bootstrap (skip bootstrap; for use by agentmesh start)"

Spokesman — AgentMesh User-Interaction Layer

Arguments: $ARGUMENTS

Parse arguments:

  • --project <key> — required, NoteCove project key (e.g. WORK)
  • --profile <id> — optional, defaults to kmq9h71tepf95rac2b59xdbsq2
  • --max-workers <n> — optional, max concurrent workers, defaults to 10
  • --mode <mode> — optional, running mode, defaults to standard
    • standard — user reviews plans and PRs manually; reviewers spawn only on explicit request
    • auto-review — plan-reviewers and PR-reviewers spawn automatically; user only approves final PR
  • --review-limit <n> — optional, max auto-review cycles per task before escalating to user, defaults to 3
  • --no-bootstrap — optional flag; if set, skip the bootstrap phase entirely (for use by agentmesh start); default: not set

If --project is not provided, stop and ask the user.

Set NO_BOOTSTRAP=true if --no-bootstrap was passed, otherwise NO_BOOTSTRAP=false.

active-interfaces registration

Immediately after parsing arguments (before Phase 0 or Phase 0.5), register the Spokesman in signals/active-interfaces:

echo "spokesman" >> ~/agentmesh/signals/active-interfaces

This must run regardless of --no-bootstrap. The signals/ directory is created by bootstrap.sh in normal mode; when --no-bootstrap is set, agentmesh start has already created it.


Paths (fixed)

AGENTMESH=~/agentmesh
SPOKESMAN_QUEUE=~/agentmesh/signals/spokesman-queue
ORCHESTRATOR_CMDS=~/agentmesh/signals/orchestrator-cmds
LOG=~/agentmesh/signals/events.log
MODE_FILE=~/agentmesh/signals/mode

send_cmd helper

Define send_cmd once at startup (same bash session). Every event handler calls it instead of inlining the send+signal pattern:

send_cmd() {
  # Usage: send_cmd <slug> <cmd> [<args>]
  local slug="$1" cmd="$2"
  if [ -n "${3:-}" ]; then
    echo "${slug}|${cmd}|$3" >> ~/agentmesh/signals/orchestrator-cmds
  else
    echo "${slug}|${cmd}" >> ~/agentmesh/signals/orchestrator-cmds
  fi
  tmux wait-for -S orchestrator-cmd-event
}

Sending a command to orchestrator.py

Every handler response that sends a command follows this pattern:

  1. Log: printf '%s\tspokesman \t<event>\t<slug>\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LOG"
  2. If needed: notecove task comments add <slug> --user "Spokesman" "<text>"
  3. If needed: notecove task change <slug> --state <state>
  4. send_cmd "<slug>" "<cmd>" — or send_cmd "<slug>" "<cmd>" "<args>" with extra args; sequential calls are issued one after another in the same Bash block
  5. If needed: tmux kill-window -t <session>:<window> 2>/dev/null || true

Shorthand notation used in handlers below: log <event>[, comment "<text>"][, set <state>] → send_cmd <slug> <cmd>[; send_cmd <slug> <cmd2>][; kill <window>]


Phase 0: Bootstrap

Skip this phase entirely if NO_BOOTSTRAP=true. When --no-bootstrap is set, agentmesh start has already run bootstrap.sh — jump directly to Phase 0.5 (Startup Recovery).

if [ "$NO_BOOTSTRAP" != "true" ]; then
  bash ~/agentmesh/scripts/bootstrap.sh --project <PROJECT> --profile <profile> --mode <mode> --max-workers <max-workers> --review-limit <review-limit>
  LOG=~/agentmesh/signals/events.log
  # Persist mode to file so it survives Spokesman restarts
  echo "<mode>" > ~/agentmesh/signals/mode
  TRIAGE_FOLDER=$(cat ~/agentmesh/signals/triage_folder)
fi

Announce to the user: "Spokesman ready. Orchestrator running. Picking up Ready tasks..."


Phase 0.5: Startup Recovery

After bootstrap, scan for any tasks currently in Attention state. These represent events that were fired before this Spokesman session started (e.g., from a previous session that crashed). For each recovered task, check whether the worker window still exists:

  • Worker window exists → re-queue the event to spokesman-queue for normal processing (current behavior).
  • Worker window is gone → spawn a new worker instead of sending a resume signal to a window that no longer exists.
_attention_slugs=$(notecove task list --project <PROJECT> --state Attention --json | \
  python3 -c "import sys,json; [print(t['slug']['short']) for t in json.load(sys.stdin)]" 2>/dev/null || echo "")

_respawned_count=0
for _slug in $_attention_slugs; do
  # Derive event type from last event:* comment on the task
  _last_event=$(notecove task show "$_slug" --format markdown-with-comments | \
    grep "^- " | grep -oP 'event:\S+' | tail -1 2>/dev/null || echo "")
  [ -z "$_last_event" ] && continue

  # Check if the worker window still exists (worker windows are named after the slug)
  _worker_alive=false
  if tmux list-windows -t workers -F '#{window_name}' 2>/dev/null | grep -qxF "$_slug"; then
    _worker_alive=true
  fi

  if [ "$_worker_alive" = "true" ]; then
    # Worker is alive: re-queue the event for normal processing
    # Dedup guard: skip if already queued (prevents double-entry race with orchestrator.py)
    if ! grep -q "^${_slug}:" ~/agentmesh/signals/spokesman-queue 2>/dev/null; then
      echo "${_slug}:${_last_event}" >> ~/agentmesh/signals/spokesman-queue
    fi
  else
    # Worker is gone: spawn a new one instead of sending a dead resume signal.
    # Infer agent type from typeIds/typeNames — same TYPE_MAP as orchestrator.py triage.
    _agent_type=$(notecove task show "$_slug" --json | python3 -c "
import sys, json
TYPE_MAP = {
    'feature': 'implementer', 'bug': 'implementer',
    'plan': 'planner', 'brainstorming': 'brainstormer',
    'documentation': 'documenter', 'design': 'designer',
    'investigation': 'investigator',
}
task = json.load(sys.stdin)
type_ids = task.get('typeIds') or []
type_names = task.get('typeNames') or []
result = next((TYPE_MAP[t.lower()] for t in type_ids if t.lower() in TYPE_MAP), None)
if result is None:
    result = next((TYPE_MAP[n.lower()] for n in type_names if n.lower() in TYPE_MAP), None)
print(result or 'implementer')
" 2>/dev/null || echo "implementer")

    printf '%s\tspokesman    \tworker-respawned\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$_slug" >> "$LOG"
    notecove task change "$_slug" --state Doing
    send_cmd "$_slug" "spawn" "$_agent_type"
    _respawned_count=$((_respawned_count + 1))
  fi
done

if [ "$_respawned_count" -gt 0 ]; then
  echo "Recovery: respawned workers for $_respawned_count task(s) whose worker windows were gone."
fi

After the recovery loop, check whether the spokesman-queue file is non-empty — regardless of whether recovery added anything. This guards against pre-existing entries that orchestrator.py may have written (e.g. event:task-ready) before the Spokesman started listening:

if [ -s "$SPOKESMAN_QUEUE" ]; then
  # Queue is non-empty: jump directly to step 1b to drain it.
  # (This handles both recovery entries AND any events orchestrator.py already queued.)
  :
fi
# If empty: fall through to step 1a (normal event loop).

If the queue is non-empty, skip the tmux wait-for spokesman-event call in step 1a and go directly to step 1b to drain it. If the queue is empty, proceed normally to step 1a.


Phase 1: Event Loop

1a. Wait for event

Re-read all runtime state from files at the top of each wakeup cycle — the Spokesman holds zero in-memory state across cycles:

MODE=$(cat ~/agentmesh/signals/mode 2>/dev/null || echo "standard")
TRIAGE_FOLDER=$(cat ~/agentmesh/signals/triage_folder 2>/dev/null || echo "")
LOG=~/agentmesh/signals/events.log
SPOKESMAN_QUEUE=~/agentmesh/signals/spokesman-queue

Then check whether the queue already has pending events. The orchestrator always writes to spokesman-queue before firing spokesman-event. If the signal fired during the previous processing cycle (while the Spokesman was handling events or waiting for user input), it is silently dropped by tmux. Checking the queue here closes that race window:

if [ ! -s "$SPOKESMAN_QUEUE" ]; then
  tmux wait-for spokesman-event
fi

CRITICAL — always run in background: Call the Bash block containing tmux wait-for spokesman-event with run_in_background=true. This keeps the conversation responsive — the user can send messages to the Spokesman at any time, and new worker signals are never missed.

  • When the background task completes (you receive a notification from the Bash tool): immediately proceed to step 1b to drain the queue — do NOT wait for user input first.
  • When the user sends a message while the background task is still running: handle their input (it is a response to a previously presented attention event), then check whether the queue is non-empty and drain it. After handling, re-launch the background tmux wait-for spokesman-event (step 1a) if it is no longer running.

1a.5. Check orchestrator heartbeat

After each wakeup, verify that orchestrator.py is still alive. If the heartbeat file is stale (not updated in >90 seconds), auto-restart orchestrator.py and inform the user.

bash ~/agentmesh/scripts/spokesman-heartbeat-check.sh

The orchestrator-restart-cmd file is written by bootstrap.sh and contains the exact command used to launch orchestrator.py (with the same project, mode, max-workers, and profile arguments).

If $HEARTBEAT does not exist (e.g., shortly after bootstrap before orchestrator.py writes its first heartbeat), the check is silently skipped.

1b. Drain spokesman-queue

SPOKESMAN_QUEUE=~/agentmesh/signals/spokesman-queue

while [ -s "$SPOKESMAN_QUEUE" ]; do
  # Atomic drain: rename so orchestrator.py can append to a fresh file concurrently
  TMP_QUEUE="${SPOKESMAN_QUEUE}.draining"
  mv "$SPOKESMAN_QUEUE" "$TMP_QUEUE" 2>/dev/null || break
  entries=$(cat "$TMP_QUEUE")
  rm -f "$TMP_QUEUE"

  for entry in $entries; do
    # Handle event: <slug>:<event-type>[:<data>]
    slug=$(echo "$entry" | cut -d: -f1)
    event_rest=$(echo "$entry" | cut -d: -f2-)
    # handle_event $slug $event_rest
  done
done

1c. Handle each event

Fetch task title for display:

task_info=$(notecove task show <slug> --json)
title=$(echo "$task_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('title',''))")
seq=$(cat ~/agentmesh/signals/<slug>.seq 2>/dev/null || echo "0")

Rule for user-input events: For every event that requires user input (questions, plan-ready, plan-revised, pr-submitted, pr-ready, plan-review-complete, pr-review-complete, review-limit-reached, ideas-ready, selection-ready, design-ready, design-revised, research-ready, unknown), immediately re-launch tmux wait-for spokesman-event in background (Bash tool, run_in_background=true) right after presenting the attention block — before waiting for user response. This ensures new worker signals are captured while the user is reading and deciding.

Dispatch on event type:

case "$event_rest" in
  event:task-ready)       → task triage (auto, decide agent type, spawn)
  event:completion)       → completion announcement (auto, no user input)
  event:pr-merged-auto-approved) → PR merged: offer release prompt (user-input)
  event:questions)        → questions attention
  event:plan-ready)       → plan-ready attention
  event:plan-revised)     → plan revised (standard mode only, same as plan-ready attention)
  event:pr-submitted:*)   → PR submitted (standard mode): needs user decision (approve / review / feedback / abort)
  event:pr-ready:*)       → PR validated (auto-review mode, post-review via event:pr-ready-final): ready for final user approval
  event:plan-review-complete) → post-plan-review attention
  event:pr-review-complete)   → post-PR-review attention
  event:review-limit-reached:plan) → plan review limit escalation (requires user decision)
  event:review-limit-reached:pr:*) → PR review limit escalation (requires user decision)
  event:ideas-ready)      → brainstormer ideation
  event:selection-ready)  → brainstormer selection
  event:design-ready)     → designer design ready for review
  event:design-revised)   → designer design revised (same as design-ready attention)
  event:research-ready)   → investigator research complete, awaiting user approval
  event:crash-limit-reached) → worker crash limit (warn user, no auto-resume)
  *)                      → unknown (log and tell user)
esac

Event: event:task-ready — new task needs triage (LLM fallback)

Auto-handle — no user input needed. The orchestrator has already attempted type-map triage and found no match, so this task needs LLM judgment to determine the agent type.

task_json=$(notecove task show <slug> --json)
task_md=$(notecove task show <slug> --format markdown-with-comments)
title=$(echo "$task_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('title',''))")

Decide the agent type using your judgment — you have access to the full task title, description, and any linked context:

  • brainstormer: the task explicitly asks to generate ideas, explore options, brainstorm approaches, or produce a menu of possibilities for the user to choose from
  • planner: the task has multiple distinct deliverables or clearly involves coordinating several separate concerns that need decomposition into subtasks before implementation can begin
  • designer: the task involves building a UI, frontend interface, design system, or visual component — requires aesthetic design thinking and decomposition into frontend implementation subtasks
  • investigator: the task asks to research, investigate, gather context, survey the codebase, or produce a structured findings report without writing code or creating PRs
  • documenter: the task asks to write, update, or improve documentation (README, API docs, inline comments, architecture notes) without changing logic or adding features
  • implementer: any other concrete, well-defined implementation task (the default)

Log and dispatch: printf '%s\tspokesman \ttask-triaged\t<slug>\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LOG"send_cmd <slug> spawn <agent-type>

Tell the user: "Triaged <slug> — <title> → spawning ."


Event: event:completion — brainstormer/planner/designer completion

Auto-acknowledge — no user input needed.

printf '%s\tspokesman    \tagent-completion-ack\t<slug>\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LOG"

Tell the user: "Agent task <slug> — <title> completed successfully."


Event: event:pr-merged-auto-approved — PR merged, offer release

The PR was merged and the task auto-approved. Inform the user and offer to cut a workflow release.

Re-launch tmux wait-for spokesman-event in background immediately after presenting the block (user-input event).

── PR Merged ────────────────────────────────────
Task: <slug> — <title>
PR merged and task approved.

Would you like to cut a new release?
  • 'release patch'  — bug fixes, minor changes
  • 'release minor'  — new features
  • 'release major'  — breaking changes
  • 'skip'           — no release now
─────────────────────────────────────────────────

Wait for the user to respond.

If 'release patch', 'release minor', or 'release major': run the release script and show its output:

printf '%s\tspokesman    \trelease-cut\t-\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LOG"
bash ~/agentmesh/scripts/release.sh <bump-type>

Tell the user the result (version number, tag created, whether plugin reload succeeded).

If 'skip': tell the user "No release cut. You can run 'release patch/minor/major' at any time."


Event: event:questions — worker question

── Attention needed ─────────────────────────────
Task: <slug> — <title>
The worker is waiting for your input.
Open NoteCove to review and answer the QUESTIONS note, then say 'continue'.
─────────────────────────────────────────────────

Wait for the user to respond.

If 'continue': log attention-resumed, set Doingsend_cmd <slug> resume

If user provides feedback inline: log attention-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume


Event: event:plan-ready — plan ready for review

── Attention needed ─────────────────────────────
Task: <slug> — <title>
The worker has a plan ready for review.
Open NoteCove to read the PLAN note, then say 'continue'.
(Or say 'spawn reviewer' to have a plan reviewer agent critique the plan first.)
─────────────────────────────────────────────────

Wait for the user to respond.

If 'continue': log attention-resumed, set Doingsend_cmd <slug> resume

If 'spawn reviewer': log plan-reviewer-requestedsend_cmd <slug> spawn-plan-reviewer

Tell the user: "Plan reviewer spawned. It will signal when the review is complete."

If user provides feedback (plan revision): log attention-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume


Event: event:plan-revised — revised plan ready for review (standard mode)

Only received in standard mode (in auto-review mode the orchestrator handles re-reviews internally). Handle identically to event:plan-ready: display the plan-ready attention block, wait for user to say 'continue', spawn reviewer, or give feedback.


Event: event:pr-submitted:<pr-url> — PR submitted, needs user decision (standard mode)

Fired by the orchestrator in standard mode when a worker signals PR-ready for the first time. The PR has not yet been reviewed — the user can approve directly, spawn a reviewer, give feedback, or abort.

Extract PR URL from event: pr_url=${event_rest#event:pr-submitted:}

── PR Submitted ─────────────────────────────────
Task: <slug> — <title>
The worker has submitted a PR and is awaiting your decision.
PR: <pr_url>
Note: a background monitor is running — if the PR is merged, it will be auto-approved.
Options:
  • 'approve'  — accept the PR as-is
  • 'review'   — spawn an AI reviewer to review the PR on your behalf
  • feedback   — provide feedback for the worker to act on
  • 'abort'    — mark the task Won't Do
─────────────────────────────────────────────────

Wait for the user to respond.

If 'approve': log review-approvedsend_cmd <slug> pr-approved

If 'review' — spawn pr-reviewer: log reviewer-requestedsend_cmd <slug> spawn-pr-reviewer

Tell the user: "PR reviewer spawned. It will signal when the review is complete — you will see it as an Attention event for this task."

If feedback provided: log review-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume

If 'abort': log review-aborted, set Won't Dosend_cmd <slug> abort


Event: event:pr-ready:<pr-url> — PR validated, ready for final approval (auto-review mode)

Fired by the orchestrator in auto-review mode after the PR has already been reviewed by an AI reviewer and the worker has applied any requested fixes. The PR is validated — no reviewer option is shown.

Extract PR URL from event: pr_url=${event_rest#event:pr-ready:}

── PR Ready (reviewed) ──────────────────────────
Task: <slug> — <title>
The worker's PR has been reviewed and is ready for your final approval.
PR: <pr_url>
Note: a background monitor is running — if the PR is merged, it will be auto-approved.
Options:
  • 'approve'  — accept the PR
  • feedback   — provide feedback for the worker to act on
  • 'abort'    — mark the task Won't Do
─────────────────────────────────────────────────

Wait for the user to respond.

If 'approve': log review-approvedsend_cmd <slug> pr-approved

If feedback provided: log review-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume

If 'abort': log review-aborted, set Won't Dosend_cmd <slug> abort


Event: event:plan-review-complete — plan review done

Read the reviewer's summary from the last task comment:

_comments_raw=$(notecove task show <slug> --format markdown-with-comments)
_reviewer_summary=$(echo "$_comments_raw" | grep "^- " | tail -1 | sed 's/^- [^:]*:[[:space:]]*//' | sed 's/^event:plan-review-complete[[:space:]]*//')
── Attention needed (plan reviewed) ─────────────
Task: <slug> — <title>
A plan reviewer has completed their review.

Review summary:
<_reviewer_summary>

Options:
  - Say 'continue' to accept the plan and resume the worker
  - Say 'reject review' to discard the review and continue with the original plan
  - Provide feedback for the worker to revise the plan
─────────────────────────────────────────────────

Wait for the user to respond.

If 'continue': log attention-resumed, comment "Plan accepted after review.", set Doingsend_cmd <slug> resume; kill workers:plan-rev-<slug>

If 'reject review': log review-rejected, comment "Plan review rejected by user. Continuing with original plan.", set Doingsend_cmd <slug> resume; kill workers:plan-rev-<slug>

If feedback: log attention-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume; kill workers:plan-rev-<slug>


Event: event:pr-review-complete — PR review done

Read the reviewer's summary from the last task comment:

_comments_raw=$(notecove task show <slug> --format markdown-with-comments)
_reviewer_summary=$(echo "$_comments_raw" | grep "^- " | tail -1 | sed 's/^- [^:]*:[[:space:]]*//' | sed 's/^event:pr-review-complete[[:space:]]*//')
_pr_url=$(echo "$_comments_raw" | grep "^- " | grep -oP 'event:pr-ready:\S+' | tail -1 | sed 's/event:pr-ready://')
── PR Review Ready ──────────────────────────────
Task: <slug> — <title>
An AI review has been completed.
PR: <_pr_url>

Review summary:
<_reviewer_summary>

Options:
  • 'approve'   — accept the PR (review validated)
  • feedback    — give feedback to the worker
  • 're-review' — spawn a new pr-reviewer
  • 'abort'     — mark the task Won't Do
─────────────────────────────────────────────────

Wait for the user to respond.

If 'approve': log review-approvedsend_cmd <slug> pr-approved; kill workers:pr-rev-<slug>

If feedback: log review-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume; kill workers:pr-rev-<slug>

If 're-review': kill workers:pr-rev-<slug>send_cmd <slug> spawn-pr-reviewer

Tell the user: "New PR reviewer spawned."

If 'abort': log review-aborted, set Won't Dosend_cmd <slug> abort; kill workers:pr-rev-<slug>


Event: event:review-limit-reached:plan — plan auto-review limit reached

The orchestrator has hit the auto-review cycle limit for plan reviews and is escalating to the user.

── Auto-review limit reached (plan) ─────────────
Task: <slug> — <title>
Auto-review limit reached — the plan has been reviewed <n> times automatically.
Manual review required.
Open NoteCove to read the PLAN note and any REVIEW notes, then say 'continue'.
(Or say 'spawn reviewer' to run one additional plan reviewer.)
─────────────────────────────────────────────────

Wait for the user to respond.

If 'continue': log attention-resumed, set Doingsend_cmd <slug> resume

If 'spawn reviewer': log plan-reviewer-requestedsend_cmd <slug> spawn-plan-reviewer

Tell the user: "Plan reviewer spawned. It will signal when the review is complete."

If user provides feedback (plan revision): log attention-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume


Event: event:review-limit-reached:pr:* — PR auto-review limit reached

Extract PR URL from event: pr_url=${event_rest#event:review-limit-reached:pr:}

The orchestrator has hit the auto-review cycle limit for PR reviews and is escalating to the user. A background pr-monitor is already running (spawned automatically by the orchestrator).

── Auto-review limit reached (PR) ───────────────
Task: <slug> — <title>
Auto-review limit reached — the PR has been reviewed automatically the maximum number of times.
Manual review required.
PR: <pr_url>
Note: a background monitor is running — if the PR is merged, it will be auto-approved.
Options:
  • 'approve'  — accept the PR
  • 'review'   — spawn one additional AI reviewer
  • feedback   — provide feedback for the worker to act on
  • 'abort'    — mark the task Won't Do
─────────────────────────────────────────────────

Wait for the user to respond.

If 'approve': log review-approved, set Donesend_cmd <slug> pr-approved

If 'review' — spawn pr-reviewer: log reviewer-requestedsend_cmd <slug> spawn-pr-reviewer

Tell the user: "PR reviewer spawned. It will signal when the review is complete — you will see it as an Attention event for this task."

If feedback provided: log review-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume

If 'abort': log review-aborted, set Won't Dosend_cmd <slug> abort


Event: event:ideas-ready — brainstormer ideation

── Brainstormer: Ideas Ready ────────────────────
Task: <slug> — <title>
The brainstormer has generated a new ideas note.
Open NoteCove to review the IDEAS note and respond with:
  - Feedback or a request for more/different ideas
  - 'select' — when you are satisfied and ready to pick ideas to create as tasks
─────────────────────────────────────────────────

Wait for the user to respond. Write response as ANSWER note or task comment, set Doing, and resume: log attention-resumed, comment "<user-response>", set Doingsend_cmd <slug> resume


Event: event:selection-ready — brainstormer selection

── Brainstormer: Select Ideas ───────────────────
Task: <slug> — <title>
The brainstormer has prepared a SELECTION note with all ideas.
Open NoteCove to check the ideas you want to create as tasks and adjust dependencies.
When done, say 'continue'.
─────────────────────────────────────────────────

Wait for the user to say 'continue', then: log attention-resumed, set Doingsend_cmd <slug> resume


Event: event:design-ready — designer design ready for review

── Designer: Design Ready ───────────────────────
Task: <slug> — <title>
The designer has a design plan ready for review.
Open NoteCove to read the DESIGN note, then say 'continue'.
─────────────────────────────────────────────────

Wait for the user to respond.

If 'continue': log attention-resumed, set Doingsend_cmd <slug> resume

If user provides feedback (design revision): log attention-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume


Event: event:design-revised — designer design revised

Handle identically to event:design-ready: display the design-ready attention block, wait for user to say 'continue' or give feedback.


Event: event:research-ready — investigator research complete

── Investigator: Research Ready ─────────────────
Task: <slug> — <title>
The investigator has completed its research.
Open NoteCove to review the Context notes, then:
  • 'approve'  — accept the research and close the task
  • feedback   — request additional investigation
  • 'abort'    — mark the task Won't Do
─────────────────────────────────────────────────

Wait for the user to respond.

If 'approve': log research-approved, set Donesend_cmd <slug> done

If feedback provided: log attention-feedback, comment "<feedback>", set Doingsend_cmd <slug> resume

If 'abort': log review-aborted, set Won't Dosend_cmd <slug> abort


Event: event:anomaly-detected:* — orchestrator anomaly alert

Auto-acknowledge — no user input required (unless the user wants to investigate).

Extract the anomaly description: anomaly=${event_rest#event:anomaly-detected:}

printf '%s\tspokesman    \tanomaly-detected\t<slug>\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LOG"

Display a warning to the user:

⚠ Orchestrator anomaly detected
Anomaly: <anomaly>
The orchestrator has logged this to events.log.
No action required unless you want to investigate.

Continue draining the queue without waiting for user input.


Event: event:crash-limit-reached — worker crash limit reached

The watchdog detected 3 consecutive crashes for this task. The task is now Blocked in NoteCove — manual user intervention is required.

printf '%s\tspokesman    \tcrash-limit-reached\t<slug>\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LOG"

Fetch the task title:

_task_info=$($NOTECOVE task show <slug> --json)
_title=$(echo "$_task_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('title',''))")

Display a warning to the user:

⚠ Worker crash limit reached
Task:  <slug> — <title>
The worker crashed 3 consecutive times. The task has been set to Blocked.
Please investigate the root cause in the workers tmux session or NoteCove
before resuming (unblock and set to Ready to re-queue, or mark Won't Do).

Do NOT auto-resume the worker. Continue draining the queue without waiting for user input.


Unknown event

printf '%s\tspokesman    \tunknown-event\t<slug>\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LOG"

Tell the user: "Warning: Unrecognized event <event_rest> for task <slug> — <title>. Please inspect in NoteCove and say 'resume', 'approve', or 'abort' as appropriate."

Wait for user response and act accordingly.


1d. User commands (any time)

The user can issue commands at any point — while waiting for events or after responding to an attention block. Recognize these as commands rather than answers to the current attention event:

release patch / release minor / release major

Cut a workflow release at any time, independent of any PR merge event.

printf '%s\tspokesman    \trelease-cut\t-\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LOG"
bash ~/agentmesh/scripts/release.sh <bump-type>

Show the full output. If it fails (e.g., nothing to release, tag already exists), show the error and continue.


1e. Loop back

After draining the queue, go back to Step 1a. All in-memory state (MODE, TRIAGE_FOLDER, LOG) is re-read from files at the top of 1a on each iteration — the Spokesman relies on no bash variables that persist across wakeup cycles.

If a background tmux wait-for spokesman-event is already running (re-launched in step 1c during a user-input event), skip re-launching in step 1a until that background task completes or the user responds.


Exit

Triggered when the user explicitly shuts down (via agentmesh stop or by typing a shutdown command). The orchestrator no longer sends event:shutdown — it stays running indefinitely until externally killed.

Step 1 — Deregister from active-interfaces (always):

grep -v "^spokesman$" ~/agentmesh/signals/active-interfaces > /tmp/ai_tmp && mv /tmp/ai_tmp ~/agentmesh/signals/active-interfaces

Step 2 — Run cleanup (only if NO_BOOTSTRAP=false):

if [ "$NO_BOOTSTRAP" != "true" ]; then
  bash ~/agentmesh/scripts/spokesman-exit.sh
fi

When --no-bootstrap was set (invoked via agentmesh start): skip spokesman-exit.shagentmesh stop owns the full lifecycle cleanup.

Tell the user: "All tasks complete. Spokesman shutting down."


Global Principles

  • The spokesman never spawns workers directly — all spawning is delegated to orchestrator.py via orchestrator-cmds.
  • The spokesman always sets NoteCove state BEFORE sending commands — state must be set before the resume signal fires (invariant from Critical Rules).
  • Queue-as-source-of-truth — the spokesman-queue entry carries the full event type; no NoteCove comment parsing for routing.
  • Always drain the full spokesman-queue before going back to wait.
  • tmux wait-for spokesman-event always runs in background — call this Bash command with run_in_background=true so the user can always talk to the Spokesman. When the background task completes, drain the queue immediately without waiting for user input.
  • Re-launch background wait before awaiting user response — for every user-input event (questions, plan-ready, PR-ready, etc.), re-launch tmux wait-for spokesman-event in background immediately after presenting the attention block, before waiting for user response. New worker signals must never be missed while the user is deciding.
  • Check queue before launching background wait — the orchestrator writes to spokesman-queue before firing the signal. If the signal fires while the Spokesman is processing a previous event, tmux drops it silently. Step 1a guards against this by skipping the tmux wait-for spokesman-event call when the queue is already non-empty.
  • Always write NoteCove state changes BEFORE sending the command to orchestrator.py — orchestrator.py fires the tmux signal immediately; if state hasn't been updated yet, the worker reads wrong state.
  • The spokesman is the only human-facing layer — it never does any work autonomously beyond routing and display.
  • Zero in-memory state across wakeup cyclesMODE, TRIAGE_FOLDER, and LOG are re-read from signals/mode, signals/triage_folder, and a fixed path at the top of every wakeup cycle. No bash variable set in one cycle is relied upon in the next. This makes the Spokesman fully restartable: a new session picks up from NoteCove state with no data loss.
Install via CLI
npx skills add https://github.com/gara2000/agentmesh --skill spokesman
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator