diff --git a/.convoys/worker-closeout-docwriter.md b/.convoys/worker-closeout-docwriter.md new file mode 100644 index 0000000..6e85fa5 --- /dev/null +++ b/.convoys/worker-closeout-docwriter.md @@ -0,0 +1,62 @@ +--- +name: worker-closeout-docwriter +classification: server-only +success_metric: Epic 7 closes — draft PR opens for code + doc_review→published for docs, operator notes work mid-build, doc-writer examples ship, live E2E checklist is runnable without Cursor +skip: + - ia + - ux + - design + - a11y + - visual + - smoke +status: in-progress +created: 2026-08-25 +model_policy: + roles: + role-conductor: claude-planning + role-architect: claude-planning + role-implementer: qwen-coder + role-doc-writer: qwen-coder + role-reviewer: qwen-coder + escalate_to: claude-planning +--- + +# Worker closeout + doc writers — closing Epic 7 for second-brain v0.1 + +## Why + +Second-brain v0.1 shipped harness-agnostic, Plans tree, Fleet. Epic 7 remains `in_progress` per `plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md:23` — tool loop + checkpoints are done, but **no draft PR** via forge adapter blocks Epic 8 dogfood, and **live E2E** `Task-worker-live-e2e-dogfood.md` needs operator proof. Doc writers were asked to be included — this convoy adds `role-doc-writer` for SOPs/book research pipelines (docs that use `doc_review→published` without worktree). + +See `docs/decisions/2026-08-25-second-brain-os-pivot.md`. + +## Scope + +1. **Draft PR via forge** `Task-worker-draft-pr.md` — push branch if needed, `createPullRequest(draft)` on GitHub/Forgejo from worktree, persist URL to convoy, skip for non-implementer roles, handle both `pr_review` and `doc_review` (doc tasks publish without PR but still log). +2. **Operator notes mid-build** `Task-worker-operator-notes.md` — `convoy_operator_messages` table (workspace-scoped, `queued|direct`), tRPC `postOperatorNote`/`listOperatorNotes`, worker poll between tool rounds, Mission control chat thread for `in_progress` convoys. +3. **Doc-writer examples** — `role-doc-writer` routed via harness (uses `doc-writer` harness `needsWorktree:false`), writes book/course research example under `docs/examples/research-pipeline.md` + `docs/templates/doc-sop.md`, updates `docs/worker.md` with doc harness section. +4. **Live E2E dogfood** `Task-worker-live-e2e-dogfood.md` — operator checklist doc (not code), proves `Go → worker` without Cursor; done only when checklist true on real Redis/Coolify. + +## Roles invoked + +Conductor → Architect (3 briefs map to 3 tasks + doc example) → Implementer per brief (disjoint: draft PR, operator notes, doc-writer) → Doc-writer (parallel) → Reviewer. + +## Todos + +- [ ] Brief 1 draft PR +- [ ] Brief 2 operator notes +- [ ] Brief 3 doc-writer examples + routing +- [ ] Brief 4 live E2E verification (ops doc, no code) + +## IA + +Skipped (`ia` in skip). No new entities beyond `convoy_operator_messages`; `harness_providers` already exists from v0.1. + +## UX + +Skipped (`ux` in skip). Reuses `Mission control detail panel` thread section + `Gate co-pilot` separation. + +## Architecture + +- State machine: `doc_review→published` already added `packages/shared/src/convoy-lifecycle/state-machine.ts:9` — reuse. +- Worker: `apps/worker/src/handle-job.ts:40` already optional worktree — add PR call after success before teardown, branch naming `echodo/-`, marker `echodo-convoy: ` in body for Epic 8 webhook. +- Operator notes: poll `convoy_operator_messages` where `delivered_at IS NULL` each `checkpointEveryNSteps`; inject `Queued note: ...` into system prompt tail or prepend `Direct message: ...`. diff --git a/.convoys/worker-closeout-docwriter/brief-1-draft-pr-forge.md b/.convoys/worker-closeout-docwriter/brief-1-draft-pr-forge.md new file mode 100644 index 0000000..7f0aa3b --- /dev/null +++ b/.convoys/worker-closeout-docwriter/brief-1-draft-pr-forge.md @@ -0,0 +1,40 @@ +--- +brief: 1 +convoy: worker-closeout-docwriter +role: role-implementer +title: Worker draft PR via forge adapter (code + doc) +status: ready +priority: P0 +slice_dependencies: [] +recommended_model: qwen-coder +model_tier: local +--- + +# Brief 1 — Draft PR `Task-worker-draft-pr.md` + +## Goal + +`apps/worker` after successful `role-implementer` session pushes worktree branch if needed, calls `createPullRequest(draft:true)` on GitHub/Forgejo (from `connected_repos` / `repo_links`), persists `prUrl`/`prNumber` to `convoy_events metadata` + `objects.content.prUrl`, marker `echodo-convoy: ` in body for Epic 8 webhook. + +## Files + +- `apps/worker/src/git/pr.ts` NEW — `openDraftPR({monorepoRoot, worktreePath, branch, convoySlug, briefId, convoyId, workspaceId, forge})` → push `git push origin HEAD` (uses stored credentials / `GITHUB_TOKEN` env pattern) → `forge.createPullRequest({base: main, head: branch, title, body, draft:true})`. Body includes `Convoy: ${slug} Brief: ${briefId} echodo-convoy: ${convoyId}`. +- `apps/worker/src/handle-job.ts:40` — after `session.run` success and before teardown, if `roleId==='role-implementer'` and `worktreePath` present, call `openDraftPR`; on doc harness `needsWorktree:false` skip push but still log `published` artifact. +- `packages/database/src/schema/convoy_events.ts` — already has `metadata jsonb`, store `prUrl`. +- `apps/web/server/lib/convoy/convoy-service.ts` — expose `prUrl` from latest `metadata.prUrl`. +- `apps/web/components/mission-control/convoy-detail-panel.tsx` — show `PR` link when `metadata.prUrl`. + +## Acceptance + +- Implementer job on test repo `stwl-labs/ubiquitous-invention` (or any connected) produces draft PR URL; non-implementer `role-architect` job skips PR creation. +- PR body contains marker consumable by Epic 8. +- Detail panel shows link when present. +- `pnpm lint && pnpm type-check && pnpm --filter @tasks/worker test` pass. + +## Prompt + +You are `role-implementer` brief 1. Read `plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-draft-pr.md`, `apps/web/server/lib/forge/adapters.ts`, `apps/worker/src/git/worktree.ts`, `docs/decisions/2026-08-25-second-brain-os-pivot.md`. Keep change small, follow existing `adapters.ts` interface. + +## Depends + +None. diff --git a/.convoys/worker-closeout-docwriter/brief-2-operator-notes.md b/.convoys/worker-closeout-docwriter/brief-2-operator-notes.md new file mode 100644 index 0000000..dc9cbc4 --- /dev/null +++ b/.convoys/worker-closeout-docwriter/brief-2-operator-notes.md @@ -0,0 +1,41 @@ +--- +brief: 2 +convoy: worker-closeout-docwriter +role: role-implementer +title: Operator notes mid-build — queued + direct messages +status: ready +priority: P1 +slice_dependencies: [] +recommended_model: qwen-coder +model_tier: local +--- + +# Brief 2 — Operator notes `Task-worker-operator-notes.md` + +## Goal + +Let operators send `queued` notes and `direct` messages to an active worker session without waiting for gate — Autensa benchmark row `operator-notes` `plans/Plan-automation-foundation/Plan-automation-foundation.md:229`. + +## Files + +- `packages/database/src/schema/convoy_operator_messages.ts` NEW — `id uuid PK, workspace_id fk, convoy_id fk objects.id, brief_id fk optional, agent_run_id fk optional, delivery enum queued|direct, body text, author_user_id fk users, delivered_at timestamptz nullable, created_at defaultNow`. Index `workspace_id, convoy_id, delivered_at`. +- `packages/database/migrations/0016_*` — `pnpm db:generate`. +- `apps/web/server/routers/convoys.ts` — add `postOperatorNote` (workspace member; `delivery:queued|direct`, `body 1..2000`, `convoyId, briefId?`) inserts row; `listOperatorNotes` for detail panel thread. +- `apps/worker/src/db/operator-notes.ts` NEW — `pollOperatorNotes(db, workspaceId, convoyId, briefId?)` where `delivered_at IS NULL` ordered `created_at`. +- `apps/worker/src/handle-job.ts:40` — between tool rounds poll queued notes, append `Queued note from ${author}: ${body}` to `toolHistory` summary; before next `generateText` prepend direct messages to system prompt tail. Mark `delivered_at=now()` after injection. Log `harnessAlias`. +- `apps/web/app/(app)/[workspaceSlug]/mission-control/convoy-detail-panel.tsx` — thread tab when `status===in_progress` (textarea + Send → `postOperatorNote`, list `listOperatorNotes`), distinct from gate co-pilot. + +## Acceptance + +- Operator posts `Queued note: use existing auth middleware` → worker sees it on next tool round (logged in checkpoint summary). +- `Direct message` arrives before next model call when run active (show in worker log `direct message injected`). +- Workspace-scoped, auditable (author + delivered_at visible in detail panel). +- `pnpm lint && pnpm type-check` pass. + +## Prompt + +You are `role-implementer` brief 2. Read `plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-operator-notes.md`, `packages/database/src/schema/convoy_events.ts`, `apps/web/server/routers/convoys.ts:173`, `docs/worker.md`. Keep polling fallback simple (no Redis pubsub). + +## Depends + +None — disjoint from brief 1. diff --git a/.convoys/worker-closeout-docwriter/brief-3-doc-writer-examples.md b/.convoys/worker-closeout-docwriter/brief-3-doc-writer-examples.md new file mode 100644 index 0000000..67050be --- /dev/null +++ b/.convoys/worker-closeout-docwriter/brief-3-doc-writer-examples.md @@ -0,0 +1,44 @@ +--- +brief: 3 +convoy: worker-closeout-docwriter +role: role-doc-writer +title: Doc-writer examples + routing for second brain (book/course/research SOPs) +status: ready +priority: P1 +slice_dependencies: [] +recommended_model: qwen-coder +model_tier: local +--- + +# Brief 3 — `role-doc-writer` for second brain (book / course / SOPs) + +## Goal + +Prove non-code second brain: `role-doc-writer` routed via harness `doc-writer` (`needsWorktree:false, allowedCommands:[]` `apps/worker/src/harness.ts:1`) writes publishable docs without `pnpm` or worktree, via `doc_review→published` `packages/shared/src/convoy-lifecycle/state-machine.ts:9`. + +## Files + +- `docs/examples/research-pipeline.md` NEW — one page: "Heavy research pipeline" example (source → claim → outline → publish), calls `doc_review→published` flow; used as doc harness e2e fixture. +- `docs/templates/doc-sop.md` NEW — copy from `docs/templates/Task-*.md` but for SOPs: `kind: doc-sop, slug, title, status: draft→doc_review→published, body`. +- `packages/shared/src/model-routing` / `harness` seeds — add doc role wiring note: `role-doc-writer → qwen-coder-mlx + doc-writer` suggested alias map `docs/adr-model-routing.md:234`. +- `docs/worker.md` — extend doc harness section (already has optional worktree note from v0.1) with `role-doc-writer` example invocation. +- `apps/web/server/routers/harnesses.ts:1` — ensure `harnessType: minimal` with `alias: doc-writer` preset is seedable (if not, document manual creation). + +## Acceptance + +- `role-doc-writer` job `docs/examples/research-pipeline.md` completes via `write_file` only (no `run_command`, no worktree under `/tmp/echodo-worker-worktrees`) and transitions `in_progress → doc_review` (`role-doc-writer` or automation), then `doc-approver` `doc_review→published` visible in Mission control. +- SOP template renders in `docs/templates/` list. +- `pnpm lint && pnpm type-check` pass. + +## Prompt + +You are `role-doc-writer` brief 3. Read `docs/decisions/2026-08-25-second-brain-os-pivot.md`, `docs/adr-model-routing.md:234`, `docs/convoy-lifecycle.md`, `packages/shared/src/convoy-lifecycle/schemas.ts:5`. Write docs that a future agent will read as context — keep them small, no narration comments. + +## Depends + +None — can run parallel with 1,2. + +## Links + +- `plans/Plan-automation-foundation/Plan-automation-foundation.md` v0.1 pivot +- `apps/worker/src/harness.ts:1` `doc-writer` harness diff --git a/apps/web/components/mission-control/convoy-detail-panel.tsx b/apps/web/components/mission-control/convoy-detail-panel.tsx index 7df6453..b052d01 100644 --- a/apps/web/components/mission-control/convoy-detail-panel.tsx +++ b/apps/web/components/mission-control/convoy-detail-panel.tsx @@ -39,6 +39,8 @@ export function ConvoyDetailPanel({ }) { const utils = api.useUtils(); const [copilotText, setCopilotText] = React.useState(null); + const [noteBody, setNoteBody] = React.useState(""); + const [noteDelivery, setNoteDelivery] = React.useState<"queued" | "direct">("queued"); const detailQuery = api.convoys.get.useQuery( { workspace: workspaceSlug, convoyId: convoyId ?? "" }, @@ -56,6 +58,18 @@ export function ConvoyDetailPanel({ onSuccess: (result) => setCopilotText(result.text), }); + const operatorNotesQuery = api.convoys.listOperatorNotes.useQuery( + { workspace: workspaceSlug, convoyId: convoyId ?? "" }, + { enabled: Boolean(workspaceSlug && convoyId) }, + ); + + const postNoteMut = api.convoys.postOperatorNote.useMutation({ + onSuccess: () => { + setNoteBody(""); + void operatorNotesQuery.refetch(); + }, + }); + if (!convoyId) { return (
@@ -106,6 +120,23 @@ export function ConvoyDetailPanel({

+ {detailQuery.data?.pr?.prUrl && ( + + )} + {convoy.description && (

Plan

@@ -177,7 +208,7 @@ export function ConvoyDetailPanel({
{(detailQuery.data?.briefs.length ?? 0) > 0 && ( -
+

Briefs

    {detailQuery.data?.briefs.map((brief) => ( @@ -189,6 +220,79 @@ export function ConvoyDetailPanel({
)} + + {status === "in_progress" && convoyId && ( +
+

+ Operator notes — in progress thread +

+

+ Distinct from gate co-pilot. Queued notes inject at next tool round; direct messages prepend before next model call. +

+
+ {(operatorNotesQuery.data ?? []).length === 0 ? ( +

No operator messages yet.

+ ) : ( + (operatorNotesQuery.data ?? []).map((note) => ( +
+
+ + {note.delivery} + + {new Date(note.createdAt).toLocaleString()} + {note.deliveredAt && ( + delivered {new Date(note.deliveredAt).toLocaleString()} + )} +
+

{note.body}

+

author {note.authorUserId.slice(0, 8)}

+
+ )) + )} +
+
+