feat(convoys): add pipeline convoy scaffolding (Epic 2)

Installs .convoys/README.md, log-convoy-event.sh, convoy-planning Cursor rule,
and gitignored metrics JSONL. Documents Echodo dogfood kickoff and adds a
shipped convoy record for Epic 1 model provider registry.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-07-07 14:14:33 -05:00
parent 2d309e8a75
commit 76dacff58d
10 changed files with 353 additions and 6 deletions

0
.convoys/.gitkeep Normal file
View file

123
.convoys/README.md Normal file
View file

@ -0,0 +1,123 @@
# Convoys (Echodo / tasks repo)
A **convoy** is a multi-PR work-stream coordinated by the agent pipeline. One convoy = one feature, bug fix, or epic slice. Each convoy is a Markdown file in this directory plus an optional sub-directory of implementer briefs.
> **Not Cursor Plan mode.** Pipeline convoys live here (`.convoys/*.md`). Cursor's native Plan feature writes to `.cursor/plans/*.plan.md` — a different artifact. For pipeline work, invoke `role-conductor` (when L2 roles are installed) and write `.convoys/<slug>.md`. See [`.cursor/rules/convoy-planning.mdc`](../.cursor/rules/convoy-planning.mdc).
Product backlog items still live under `plans/` and sync to Echodo via markdown import. Convoys track **how** a slice is executed (roles, briefs, gates) — not the canonical plan tree.
## File layout
```text
.convoys/
├── README.md (this file)
├── .metrics.jsonl (local analytics — gitignored)
├── <slug>.md (convoy file — role-conductor)
└── <slug>/
├── brief-1-<kebab-title>.md (role-architect)
└── ...
```
## Convoy file format
Frontmatter (set by `role-conductor`, appended by other roles):
```yaml
---
name: <kebab-slug>
classification: feature | hotfix | docs-only | infra-only | server-only | config-only
success_metric: <one sentence>
skip:
- ia
status: open | in-progress | merged | shipped | abandoned
created: <YYYY-MM-DD>
model_policy:
roles:
role-conductor: claude-planning
role-implementer: qwen-coder
escalate_to: claude-planning
---
```
Use **registry alias names** in `model_policy` (not Cursor model slugs). See [docs/adr-model-routing.md](../docs/adr-model-routing.md).
Body sections (typical order):
1. `## Why` (Conductor)
2. `## Scope` (Conductor)
3. `## Roles invoked` (Conductor)
4. `## Todos` (Conductor → Architect)
5. `## IA` (IA Architect — skip if `ia` in `skip:`)
6. `## UX` (UX Reviewer)
7. `## Architecture` (Architect)
After Architect, briefs live in `.convoys/<slug>/brief-N-*.md`. Implementers read only their brief.
## Skip flags
| Flag | Skips |
| --- | --- |
| `ia` | IA Architect |
| `ux` | UX Reviewer |
| `arch` | Architect (rare — use only for trivial hotfixes) |
| `test` | Component tests |
| `review` | Reviewer |
| `visual` | Visual diff |
| `a11y` | A11y auditor |
| `design` | Design-system auditor |
| `smoke` | Staging smoke |
| `qa` | Manual QA |
| `docs` | Doc Writer |
| `flag` | Flag rollout |
Never skipped (human gates): `plan-approval`, `pr-merge`, `prod-promote`.
**Echodo server work** often uses `classification: server-only` with `skip: [ia, ux, design, a11y, visual, smoke]`.
## Status lifecycle
- `open` — convoy created; no briefs in flight
- `in-progress` — at least one brief active or PR open
- `merged` — all briefs merged; release pending
- `shipped` — in production
- `abandoned` — closed without shipping
## Kickoff ritual (this repo)
1. Open Cursor in the `tasks` repo on a feature branch.
2. Prompt: *"Start a new convoy: \<idea\>. Success = \<metric\>."*
3. Conductor writes `.convoys/<slug>.md` with `classification` and `skip` flags.
4. **Echodo dogfood:** each implementer session should `claim_task` on the matching `plans/.../Task-*.md`, implement, then `complete_task` with `modelAlias` / `modelTier`.
5. Architect maps briefs 1:1 to tasks when possible.
**Example (Epic 1 — done):** model provider registry convoy with `server-only`, briefs aligned to `Epic-model-provider-registry` tasks.
**Next convoys:** orchestrator (`Epic-apps-orchestrator`), convoy lifecycle MCP (`Epic-convoy-lifecycle-mcp`), connected repos (`Epic-connected-repos`).
## Self-analytics
Each L2 role appends one event via:
```bash
bash scripts/log-convoy-event.sh role=role-implementer convoy=<slug> brief=1 \
duration_s=120 model=qwen-coder model_tier=local outcome=complete
```
`.convoys/.metrics.jsonl` is **gitignored** by default (metadata only, no prompts). Remove that line from `.gitignore` if you want team-visible metrics in git.
Validate a line was written:
```bash
bash scripts/log-convoy-event.sh role=role-conductor convoy=test duration_s=1
tail -1 .convoys/.metrics.jsonl | python3 -m json.tool
```
## Multitask (Cursor 3.2+)
Use `/multitask` for audit fan-out (`role-reviewer` + design + a11y) or parallel implementers when architect marks briefs as disjoint. Set `multitask_group=audit-<convoy>-<pr>` on log lines for wall-clock analytics.
## Related docs
- [Plan automation foundation](../plans/Plan-automation-foundation/Plan-automation-foundation.md)
- [ADR model routing](../docs/adr-model-routing.md)
- [AGENTS.md](../AGENTS.md) — repo commands and multitenancy rules

View file

@ -0,0 +1,53 @@
---
name: model-provider-registry
classification: server-only
success_metric: Operators configure Anthropic + local coder aliases and per-role routing from Settings; agent runs log modelAlias.
skip:
- ia
- ux
- design
- a11y
- visual
- smoke
status: shipped
created: 2026-07-07
model_policy:
roles:
role-conductor: claude-planning
role-architect: claude-planning
role-implementer: qwen-coder
role-reviewer: qwen-coder
escalate_to: claude-planning
---
# Model provider registry (Epic 1)
## Why
Echodo needs a workspace-scoped model registry and `resolveModel()` contract before orchestrator/worker can dispatch unattended jobs. Epic 1 on `feat/automation-foundation` delivered the full slice.
## Scope
- `packages/shared` — schemas + `resolveModel()`
- `model_providers` / `role_model_routes` tables
- `api.ai.models` tRPC + Settings UI
- `agent_runs` model columns + MCP `complete_task` fields
## Roles invoked
Conductor → Architect (briefs = Epic 1 tasks) → Implementer (per task) → Reviewer (PR).
## Outcome
Shipped in PR on `feat/automation-foundation`. Epic 2 convoy scaffolding added after merge for ongoing automation-foundation work.
## Briefs (architect mapping)
| Brief | Task slug |
| --- | --- |
| 1 | `shared-schemas-resolve-model` |
| 2 | `db-model-providers-migration` |
| 3 | `trpc-models-router` |
| 4 | `settings-ai-providers-ui` |
| 5 | `settings-ai-role-routing-ui` |
| 6 | `agent-runs-model-logging` |

View file

@ -0,0 +1,35 @@
---
description: Pipeline planning uses .convoys/*.md — not Cursor Plan files in .cursor/plans/
alwaysApply: true
---
# Convoy planning (pipeline work)
When scoping, planning, or kicking off **pipeline work** (features, hotfixes, epics tracked by L2 roles), use **convoy files** — not Cursor's native Plan mode.
## Where plans live
| Use case | Location | Who writes it |
| --- | --- | --- |
| **Product backlog** (Plan/Epic/Task) | `plans/` → Echodo DB | Humans + markdown import |
| **Pipeline convoy** (idea → briefs → PR) | `.convoys/<slug>.md` | `role-conductor`, then other L2 roles |
| **Implementer briefs** | `.convoys/<slug>/brief-<N>-<title>.md` | `role-architect` |
| **Cursor Plan mode** (ad-hoc, non-pipeline) | `.cursor/plans/*.plan.md` | Cursor UI — **not** the agent-pipeline |
## Do
- Start pipeline work: *"Run role-conductor: start a new convoy for [idea]. Success = [metric]."*
- Read [`.convoys/README.md`](mdc:.convoys/README.md) for frontmatter, skip flags, and lifecycle.
- Use `model_policy.roles` with **registry alias names** per [docs/adr-model-routing.md](mdc:docs/adr-model-routing.md).
- Log role completion: `bash scripts/log-convoy-event.sh role=... convoy=...` when the script exists.
- Dogfood Echodo: `claim_task` / `complete_task` on the matching `plans/**/Task-*.md` during implementer sessions.
## Do not
- Create `.cursor/plans/` files for convoy or L2-role work.
- Store convoy execution state only in chat when a `.convoys/<slug>.md` file should exist.
- Put secrets or API keys in convoy frontmatter — use `model_policy` alias names only.
## If a `.cursor/plans/` file already exists
That is Cursor's native Plan artifact, not a pipeline convoy. Copy useful content into `.convoys/<slug>.md` and continue from the convoy file.

3
.gitignore vendored
View file

@ -55,3 +55,6 @@ docker/**/data/
# misc
*.pem
coverage/
# Convoy pipeline metrics (local-only by default; see .convoys/README.md)
.convoys/.metrics.jsonl

View file

@ -3,11 +3,14 @@ kind: epic
slug: pipeline-convoy-scaffolding
title: Pipeline convoy scaffolding — .convoys/ and metrics in tasks repo
plan_slug: automation-foundation
status: draft
status: done
priority: P1
tenant_id: global
cursor_epic_id: null
updated_at: "2026-07-03"
updated_at: "2026-07-07"
agent_prompt: |
Epic: pipeline convoy scaffolding. Install .convoys/, log-convoy-event.sh,
convoy-planning.mdc. No MCP or orchestrator code. claim_task before work.
---
# Epic objective
@ -38,9 +41,12 @@ Can run **in parallel** with Epic 1 (model provider registry).
## Acceptance criteria
- [ ] `bash scripts/log-convoy-event.sh role=role-conductor convoy=test` appends valid JSONL.
- [ ] `role-conductor` can write `.convoys/<slug>.md` without conflicting rules.
- [x] `bash scripts/log-convoy-event.sh role=role-conductor convoy=test` appends valid JSONL.
- [x] `role-conductor` can write `.convoys/<slug>.md` without conflicting rules.
## Tasks
_To be decomposed by architect when epic graduates to `ready`._
| Task | Link |
|------|------|
| log-convoy-event script | `./Task-log-convoy-event-script.md` |
| README + planning rule | `./Task-convoy-readme-and-planning-rule.md` |

View file

@ -0,0 +1,22 @@
---
kind: task
slug: convoy-readme-and-planning-rule
title: .convoys/README.md and convoy-planning Cursor rule
plan_slug: automation-foundation
epic_slug: pipeline-convoy-scaffolding
status: done
priority: P1
tenant_id: global
owner: unassigned
cursor_todo_id: null
updated_at: "2026-07-07"
---
# Task summary
Document convoy layout, kickoff ritual, and Echodo dogfood (`claim_task` / registry aliases). Add always-on `.cursor/rules/convoy-planning.mdc` so agents use `.convoys/` instead of Cursor Plan mode.
## Acceptance criteria
- [x] README covers frontmatter, skip flags, model_policy aliases (ADR)
- [x] `convoy-planning.mdc` distinguishes `plans/` vs `.convoys/` vs `.cursor/plans/`

View file

@ -0,0 +1,22 @@
---
kind: task
slug: log-convoy-event-script
title: scripts/log-convoy-event.sh and .gitignore metrics entry
plan_slug: automation-foundation
epic_slug: pipeline-convoy-scaffolding
status: done
priority: P1
tenant_id: global
owner: unassigned
cursor_todo_id: null
updated_at: "2026-07-07"
---
# Task summary
Add `scripts/log-convoy-event.sh` (adapted from agent-pipeline L3) and gitignore `.convoys/.metrics.jsonl`.
## Acceptance criteria
- [x] `bash scripts/log-convoy-event.sh role=role-conductor convoy=test` appends valid JSONL
- [x] Script is executable and uses python3 for safe JSON encoding

View file

@ -163,7 +163,7 @@ flowchart LR
| # | Epic | Status | Delivers | Depends on |
|---|------|--------|----------|------------|
| 1 | [Model provider registry](./Epic-model-provider-registry/Epic-model-provider-registry.md) | **ready** (6 tasks) | `resolveModel()`, Settings AI pages, run model logging | — |
| 2 | [Pipeline convoy scaffolding](./Epic-pipeline-convoy-scaffolding/Epic-pipeline-convoy-scaffolding.md) | draft | `.convoys/`, `log-convoy-event.sh`, `convoy-planning.mdc` — dogfood metrics | — (parallel with 1) |
| 2 | [Pipeline convoy scaffolding](./Epic-pipeline-convoy-scaffolding/Epic-pipeline-convoy-scaffolding.md) | done | `.convoys/`, `log-convoy-event.sh`, `convoy-planning.mdc` — dogfood metrics | — (parallel with 1) |
| 3 | [Connected repos](./Epic-connected-repos/Epic-connected-repos.md) | draft | `repo_links` table, “paste remote URL”, forge adapter interface | — |
| 4 | [Convoy lifecycle MCP](./Epic-convoy-lifecycle-mcp/Epic-convoy-lifecycle-mcp.md) | draft | `create_convoy`, `transition_convoy_status`, state machine §7, `convoy_events` | 2, 3 |
| 5 | [Mission control gates UI](./Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md) | draft | Convoy kanban, gate inbox, approve/deny/revise, gate co-pilot chat | 4 |

83
scripts/log-convoy-event.sh Executable file
View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
# log-convoy-event.sh — append one convoy event to .convoys/.metrics.jsonl.
#
# Used by L2 roles to emit lightweight metrics for self-analytics.
# Schema: github.com/varutasu/agent-pipeline/analytics/schemas/convoy-event.json
#
# Usage:
# bash scripts/log-convoy-event.sh role=role-conductor convoy=bookmark-badge \
# classification=feature 'skip_flags=visual,smoke' duration_s=42 \
# model=claude-planning model_tier=premium
#
# All args are key=value. Required: role, convoy.
# Optional: brief, classification, skip_flags (comma-separated), duration_s,
# stack_class, outcome, multitask_group, model, model_tier,
# estimated_cost_usd.
#
# model / model_tier: use registry alias names per docs/adr-model-routing.md.
#
# Privacy: this file is gitignored by default; events contain only metadata,
# no code or prompts.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
REPO_NAME="$(basename "$REPO_ROOT")"
METRICS_FILE="$REPO_ROOT/.convoys/.metrics.jsonl"
mkdir -p "$REPO_ROOT/.convoys"
ROLE=""; CONVOY=""; BRIEF=""; CLASSIFICATION=""
SKIP_FLAGS=""; DURATION_S=""; STACK_CLASS=""; OUTCOME=""; MULTITASK_GROUP=""
MODEL=""; MODEL_TIER=""; ESTIMATED_COST_USD=""
for arg in "$@"; do
k="${arg%%=*}"
v="${arg#*=}"
case "$k" in
role) ROLE="$v" ;;
convoy) CONVOY="$v" ;;
brief) BRIEF="$v" ;;
classification) CLASSIFICATION="$v" ;;
skip_flags) SKIP_FLAGS="$v" ;;
duration_s) DURATION_S="$v" ;;
stack_class) STACK_CLASS="$v" ;;
outcome) OUTCOME="$v" ;;
multitask_group) MULTITASK_GROUP="$v" ;;
model) MODEL="$v" ;;
model_tier) MODEL_TIER="$v" ;;
estimated_cost_usd) ESTIMATED_COST_USD="$v" ;;
*) echo "log-convoy-event: ignoring unknown arg '$k'" >&2 ;;
esac
done
if [ -z "$ROLE" ] || [ -z "$CONVOY" ]; then
echo "log-convoy-event: role and convoy are required" >&2
echo "Usage: $0 role=<role> convoy=<slug> [classification=...] [skip_flags=a,b] [duration_s=N] [brief=N] [model=...] [model_tier=premium|fast|local|auto] [estimated_cost_usd=N]" >&2
exit 1
fi
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
python3 - <<PY >> "$METRICS_FILE"
import json, sys
ev = {
"ts": "$ts",
"role": $(printf '%s' "$ROLE" | python3 -c 'import json,sys;print(json.dumps(sys.stdin.read()))'),
"convoy": $(printf '%s' "$CONVOY" | python3 -c 'import json,sys;print(json.dumps(sys.stdin.read()))'),
"repo": $(printf '%s' "$REPO_NAME" | python3 -c 'import json,sys;print(json.dumps(sys.stdin.read()))'),
"skip_flags": [s for s in "$SKIP_FLAGS".split(",") if s],
}
if "$BRIEF": ev["brief"] = int("$BRIEF")
if "$CLASSIFICATION": ev["classification"] = "$CLASSIFICATION"
if "$DURATION_S": ev["duration_s"] = int("$DURATION_S")
if "$STACK_CLASS": ev["stack_class"] = "$STACK_CLASS"
if "$OUTCOME": ev["outcome"] = "$OUTCOME"
if "$MULTITASK_GROUP": ev["multitask_group"] = "$MULTITASK_GROUP"
if "$MODEL": ev["model"] = "$MODEL"
if "$MODEL_TIER": ev["model_tier"] = "$MODEL_TIER"
if "$ESTIMATED_COST_USD":
ev["estimated_cost_usd"] = float("$ESTIMATED_COST_USD")
print(json.dumps(ev))
PY
echo "Logged: role=$ROLE convoy=$CONVOY → .convoys/.metrics.jsonl"