docs(taskmaster): PRD + task graph for agentic teams foundation
Converts docs/agentic-teams.md §16 steps 1-3 into a trackable PRD and 48 appended tasks (IDs 115-162) in the master tag. Steps 4-8 land as 5 low priority placeholders wired onto the spine, pending the §15 open questions. The PRD is grounded in the current codebase rather than the design doc's indicative sketches, which changed the shape of the work: - RBAC `tasks` is already owned by the inference queue, and bindings are normalized at parse time, so existing RbacDefinition rows literally hold "tasks". Taking the name for work items needs a one-shot guarded backfill to `inference-tasks` (task 116). - deploy/entrypoint.sh uses `prisma db push`, so migration SQL never runs in production. Invariants are service-layer, not CHECK constraints. - Ephemeral pods do not exist: restartPolicy is hardcoded Always and ContainerSpec has no volume/workdir fields (tasks 140-142). - `exec` is cheap: KubernetesOrchestrator already implements exec/attach over k8s.Exec/k8s.Attach; only an API + CLI surface is missing (tasks 144-145). - No git provider client exists in product code (task 148). - ChatService caps its tool loop at 12 iterations, so the AgentRuntime runs a headless coding agent in-pod rather than extending chat (tasks 146-147). Names the git-repo entity CodeRepo (CLI `repos`) to avoid colliding with the repository/ data-access layer; AgentProject carries an optional llmId so the brain-ownership question can be settled either way without a migration. Generated task_*.md files are left untracked, matching how this repo has treated them since the first commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
This commit is contained in:
808
.taskmaster/docs/prd-agentic-teams.md
Normal file
808
.taskmaster/docs/prd-agentic-teams.md
Normal file
@@ -0,0 +1,808 @@
|
||||
# PRD: Agentic Teams — Foundation
|
||||
|
||||
## Overview
|
||||
|
||||
mcpctl today orchestrates MCP servers, projects, prompts, skills, and chat agents. It does not
|
||||
orchestrate *work*. This PRD adds the foundation that lets human developers and autonomous agents
|
||||
be peers in the same project: a versioned vision layer (Epic → epictask → task), git repositories
|
||||
attached to projects, agents as many-to-many project members, ephemeral per-session pods where
|
||||
agents do real work, a swappable agent runtime, a task lifecycle with QA/Security/SRE gates, and a
|
||||
single human accept gate that fires a merge through the git provider's API.
|
||||
|
||||
The design record is `docs/agentic-teams.md`. This PRD converts §16 steps 1–3 of that document
|
||||
into an implementation spec. Steps 4–8 are named as deferred work at the end, deliberately
|
||||
without detail — they depend on open design questions (`docs/agentic-teams.md` §15) that are not
|
||||
closed.
|
||||
|
||||
## Problem
|
||||
|
||||
Two failure classes motivate this work.
|
||||
|
||||
**Nothing tracks work.** There is no Epic, EpicTask, Task, Repository, or AgentProject model in
|
||||
`src/db/prisma/schema.prisma`, and no repository, service, route, or CLI code for any of them.
|
||||
Agents can chat and call tools, but there is no unit of work to assign, no lifecycle to advance,
|
||||
no branch or PR to attach, and no gate for a human to accept.
|
||||
|
||||
**Completed tasks do not add up to the vision.** From the design doc: you have a grand vision, you
|
||||
split it into tasks, you complete all the tasks, and you are left with a stick with wheels instead
|
||||
of a car. The root cause is not execution — it is that decomposition is lossy, tasks are graded
|
||||
mechanically ("compiles / lint / renders") rather than against intent, and integration is nobody's
|
||||
job. The fix is an authoritative, versioned Epic that every task can walk up to, so "what am I
|
||||
serving, and what does done-for-this look like" is always answerable.
|
||||
|
||||
A third, practical problem shapes the ordering: the design document's "don't rebuild, delegate"
|
||||
principle is correct in spirit, but several primitives it assumes do not exist in mcpctl yet.
|
||||
Those are enumerated under **Execution Substrate Changes** and must land before any agent can work
|
||||
a task.
|
||||
|
||||
## Scope & Non-Goals
|
||||
|
||||
**In scope** (design doc §16 steps 1–3):
|
||||
|
||||
1. Epic / EpicTask / Task / CodeRepo entities + AgentProject membership, with project linkage,
|
||||
mcpd CRUD, CLI verbs, RBAC, backup, docs, and tests.
|
||||
2. An `AgentRuntime` adapter interface and a first implementation running a headless coding agent
|
||||
inside an ephemeral per-session pod. Prove: pod spawn → clone repos → work a task branch →
|
||||
push → report.
|
||||
3. Task lifecycle with QA / Security / SRE stages, PR linkage, the human accept gate, and merge
|
||||
through the git provider API, with post-merge failure reopening the task.
|
||||
|
||||
**Explicitly out of scope** — these are named in *Deferred* below and get placeholder tasks only:
|
||||
the Tier-2 design/product critic and Epic-comparison loop; collaboration modes (standup, joint
|
||||
`exec` sessions as a workflow); the confidence signal and escalation dashboard; per-persona memory
|
||||
and the coverage ledger; script artifacts and the baseline scorecard.
|
||||
|
||||
**Not being built at all:** a permission engine. mcpctl enforces coarse gates (who may transition
|
||||
a task, what RBAC a session needs); fine-grained per-tool approval stays inside the agent runtime.
|
||||
|
||||
## Decisions Already Made
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Work-item RBAC name | Work items take `tasks`; the inference queue moves to `inference-tasks` | Matches the design doc's vocabulary. The `inference-tasks` alias already exists, so the URL prefix `/api/v1/inference-tasks` is unchanged |
|
||||
| Roadmap-node spelling | Model `EpicTask`, resource `epictasks`, aliases `epictask`, `et` | Design doc §15 Q2; one word, matches the doc's own usage |
|
||||
| Git-repo resource name | Model `CodeRepo`, resource `repos`, aliases `repo`, `repository`, `repositories` | Avoids `repository.repository.ts` and the collision with the data-access-layer term used throughout `src/mcpd/src/repositories/` |
|
||||
| Epic revisions | Epic joins the existing `ResourceRevision` system as `resourceType: 'epic'` | The Epic is the auditable source of truth for "is this done"; scope changes must be visible. `ResourceType` is a string union by design, so this is a non-migration change |
|
||||
| Agent membership | New `AgentProject` junction; `Agent.projectId` retained and kept in sync for one release | Design doc §4. `ProjectMember` was deliberately dropped in migration `20260223120000_remove_project_members`; this is a narrower reversal — agent membership only, not human membership, which stays RBAC-driven |
|
||||
| Invariants | Enforced in the service layer, not CHECK constraints | See *Deployment constraint* below |
|
||||
| Agent brain | Home brain (`Agent.llmId`) for bare chat; the project's `llmProvider`/`llmModel` when the agent acts inside a project; optional per-membership override on `AgentProject.llmId` | Design doc §15 Q1 leans this way. **This is the one open question that blocks the data model** — the `AgentProject.llmId` column exists so the decision can be reversed without a migration |
|
||||
|
||||
### Deployment constraint (affects every schema task)
|
||||
|
||||
`deploy/entrypoint.sh` provisions the database with `prisma db push --accept-data-loss`, not
|
||||
`prisma migrate deploy`. Hand-written SQL in `src/db/prisma/migrations/` therefore **never runs in
|
||||
production**. The single existing CHECK constraint, `ChatThread_agent_xor_project` (migration
|
||||
`20260717234728_add_project_chat_threads`), is absent from any `db push`-provisioned database and
|
||||
is enforced only by `ChatService`.
|
||||
|
||||
Consequence: every invariant introduced here must be either expressible in the Prisma schema
|
||||
(unique constraints, non-null columns, enums, foreign keys) or enforced in the service layer with
|
||||
a test. Do not write CHECK constraints and assume they hold.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### The vision layer
|
||||
|
||||
```
|
||||
Epic (the vision — project-scoped, versioned, revision-tracked)
|
||||
└── EpicTask — a roadmap node; chains to other epictasks (dependency graph)
|
||||
└── Task — the unit of work, with its own lifecycle
|
||||
```
|
||||
|
||||
An **Epic** holds the agreed done-criteria written at vision time, its scope-change history, and
|
||||
accepted-tradeoff decisions. An Epic is done only when 100% of the vision is delivered, **or** the
|
||||
user explicitly accepted the tradeoffs — recorded so "done" is honest and auditable.
|
||||
|
||||
An **EpicTask** is a roadmap node. EpicTasks chain to one another, so visualising them yields a
|
||||
dependency graph rather than a flat sequence. Each carries the decomposed subset of the Epic's
|
||||
acceptance criteria, so per-node completeness is computable.
|
||||
|
||||
A **Task** is the unit of work, owned by an EpicTask. Because Task → EpicTask → Epic, every task
|
||||
can resolve what it is serving by walking up the chain.
|
||||
|
||||
### Two-level verification
|
||||
|
||||
| Level | Checks | Who | When |
|
||||
|---|---|---|---|
|
||||
| Tier 1 — correctness / security / ops | Builds, runs, passes tests, stays secure, and the deployment-as-code is right for this project's real infrastructure | Developer + QA + Security + SRE personas | Inside the task's own lifecycle, before Done |
|
||||
| Tier 2 — vision fidelity | Does the assembled result match the vision | Design/product critic | At the Epic level — **deferred, not in this PRD** |
|
||||
|
||||
Tier 1 stages are gates *inside* the task loop. Their feedback bounces the task back to
|
||||
`InProgress`. They are not post-submit reviewers.
|
||||
|
||||
The **SRE/Release stage** reviews the deployment-as-code — is it targeting the right
|
||||
infrastructure, by the right method, with the right tooling. It reviews the deployment
|
||||
*definition*; it does not deploy. It exists because a large share of failures come from implicit
|
||||
operational rules a developer LLM does not know. Concrete example: this cluster runs default-deny
|
||||
network policies, so any new workload needs explicit egress and ingress rules. A developer LLM
|
||||
watching a connection fail will deep-dive the network-debugging path when the real cause is a
|
||||
missing egress policy silently dropping traffic. The SRE persona, holding per-project infra
|
||||
knowledge, flags that in review.
|
||||
|
||||
### Agents as people
|
||||
|
||||
An agent exists independently of any project — a persistent identity with a name, description,
|
||||
system prompt, and a home brain. It joins zero, one, or many projects. Per-project role and
|
||||
scope hang off the membership junction, not off the agent.
|
||||
|
||||
### Pod per session
|
||||
|
||||
Each agent session gets its own ephemeral pod. The pod *is* the workspace: an init container
|
||||
clones the project's repositories, the session is attached to project prompts, MCP tools, and
|
||||
scoped secrets, and the pod dies at session end. Agents do not share a live filesystem with each
|
||||
other — handoffs happen via git (branch + diff + verdict pinned to a commit). A single human may
|
||||
share one agent's environment via `exec`.
|
||||
|
||||
## Schema Changes
|
||||
|
||||
All new models follow existing house conventions: `id String @id @default(cuid())`,
|
||||
`version Int @default(1)` as an optimistic-concurrency counter bumped unconditionally in the
|
||||
repository layer (see `agent.repository.ts` `updateData.version = { increment: 1 }`), plus
|
||||
`createdAt` / `updatedAt`.
|
||||
|
||||
### Epic
|
||||
|
||||
```prisma
|
||||
model Epic {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String @default("")
|
||||
// The agreed statement of the vision. Body of the Epic.
|
||||
content String @db.Text
|
||||
// Checkable outcomes + intent/quality attributes that define 100% delivery.
|
||||
// Array of { id, text, kind: 'outcome' | 'quality', met: boolean }.
|
||||
doneCriteria Json @default("[]")
|
||||
// Accepted-tradeoff records: { what, why, who, deScopedBy, at }.
|
||||
acceptedTradeoffs Json @default("[]")
|
||||
projectId String
|
||||
status EpicStatus @default(open)
|
||||
semver String @default("0.1.0")
|
||||
currentRevisionId String?
|
||||
ownerId String
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
epicTasks EpicTask[]
|
||||
|
||||
@@unique([name, projectId])
|
||||
@@index([projectId])
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
enum EpicStatus {
|
||||
open
|
||||
delivered // 100% of doneCriteria met
|
||||
delivered_with_tradeoffs
|
||||
cancelled
|
||||
}
|
||||
```
|
||||
|
||||
### EpicTask
|
||||
|
||||
```prisma
|
||||
model EpicTask {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String @default("")
|
||||
epicId String
|
||||
// Decomposed subset of the Epic's doneCriteria this node is responsible for.
|
||||
doneCriteria Json @default("[]")
|
||||
status EpicTaskStatus @default(todo)
|
||||
ownerId String
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
epic Epic @relation(fields: [epicId], references: [id], onDelete: Cascade)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
tasks Task[]
|
||||
dependsOn EpicTaskEdge[] @relation("EpicTaskEdgeFrom")
|
||||
blocks EpicTaskEdge[] @relation("EpicTaskEdgeTo")
|
||||
|
||||
@@unique([name, epicId])
|
||||
@@index([epicId])
|
||||
}
|
||||
|
||||
// Chaining between roadmap nodes. Modelled as an explicit edge table rather
|
||||
// than a self-referencing array so the graph can be walked in both directions
|
||||
// and edges can later carry a type.
|
||||
model EpicTaskEdge {
|
||||
id String @id @default(cuid())
|
||||
fromId String // the dependent node
|
||||
toId String // the prerequisite node
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
from EpicTask @relation("EpicTaskEdgeFrom", fields: [fromId], references: [id], onDelete: Cascade)
|
||||
to EpicTask @relation("EpicTaskEdgeTo", fields: [toId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([fromId, toId])
|
||||
@@index([fromId])
|
||||
@@index([toId])
|
||||
}
|
||||
|
||||
enum EpicTaskStatus {
|
||||
todo
|
||||
in_progress
|
||||
done
|
||||
cancelled
|
||||
}
|
||||
```
|
||||
|
||||
Cycle prevention in the edge graph is a service-layer check (see *Deployment constraint*): before
|
||||
creating an edge, walk `to`'s transitive dependencies and reject if `from` is reachable.
|
||||
|
||||
### Task
|
||||
|
||||
```prisma
|
||||
model Task {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
title String
|
||||
description String @default("") @db.Text
|
||||
epicTaskId String
|
||||
repoId String?
|
||||
// The agent currently responsible. Null while unassigned.
|
||||
assigneeId String?
|
||||
state TaskState @default(todo)
|
||||
// Drives how much Tier-1 verification is engaged: trivial work may pass on a
|
||||
// light self-check; risky work engages QA / Security / SRE.
|
||||
risk TaskRisk @default(normal)
|
||||
// Git linkage, populated when the agent pushes.
|
||||
branch String?
|
||||
headCommit String?
|
||||
prUrl String?
|
||||
prNumber Int?
|
||||
// Free-form reviewer comment attached at the accept gate.
|
||||
reviewNote String? @db.Text
|
||||
ownerId String
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
epicTask EpicTask @relation(fields: [epicTaskId], references: [id], onDelete: Cascade)
|
||||
repo CodeRepo? @relation(fields: [repoId], references: [id], onDelete: SetNull)
|
||||
assignee Agent? @relation(fields: [assigneeId], references: [id], onDelete: SetNull)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
verdicts TaskVerdict[]
|
||||
sessions AgentSession[]
|
||||
events TaskEvent[]
|
||||
|
||||
@@unique([name, epicTaskId])
|
||||
@@index([epicTaskId])
|
||||
@@index([state])
|
||||
@@index([assigneeId])
|
||||
}
|
||||
|
||||
enum TaskState {
|
||||
todo
|
||||
in_progress
|
||||
building // compile, run, smoke
|
||||
self_testing // developer's own tests
|
||||
qa_review
|
||||
security_review
|
||||
sre_review
|
||||
done // leaves the developer's loop
|
||||
awaiting_review // human accept gate
|
||||
merged
|
||||
reopened // post-merge failure
|
||||
cancelled
|
||||
}
|
||||
|
||||
enum TaskRisk {
|
||||
trivial // docs, formatting — light self-check may suffice
|
||||
normal
|
||||
high // auth, secrets, migrations, cross-contract
|
||||
}
|
||||
|
||||
// A Tier-1 persona's verdict, pinned to the commit it judged. Immutable —
|
||||
// this is the provenance record the design doc calls "QA verified 3f2ab".
|
||||
model TaskVerdict {
|
||||
id String @id @default(cuid())
|
||||
taskId String
|
||||
stage TaskState // qa_review | security_review | sre_review
|
||||
agentId String?
|
||||
commit String
|
||||
passed Boolean
|
||||
summary String @db.Text
|
||||
// Structured findings: [{ severity, message, path?, line? }]
|
||||
findings Json @default("[]")
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||
agent Agent? @relation(fields: [agentId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([taskId])
|
||||
@@index([taskId, stage])
|
||||
}
|
||||
|
||||
// Append-only transition log. Every state change writes one row.
|
||||
model TaskEvent {
|
||||
id String @id @default(cuid())
|
||||
taskId String
|
||||
fromState TaskState?
|
||||
toState TaskState
|
||||
actorType String // 'user' | 'agent' | 'system'
|
||||
actorId String?
|
||||
note String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([taskId, createdAt])
|
||||
}
|
||||
```
|
||||
|
||||
### CodeRepo
|
||||
|
||||
```prisma
|
||||
model CodeRepo {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
projectId String
|
||||
// Clone URL. https for token auth, ssh for key auth.
|
||||
url String
|
||||
defaultBranch String @default("main")
|
||||
// Relative path under the session workspace root. Defaults to `name`.
|
||||
path String @default("")
|
||||
// Git provider, for PR/merge API calls. 'gitea' is the only driver in v1.
|
||||
provider String @default("gitea")
|
||||
// Secret holding the provider credential. Resolved through SecretService at
|
||||
// session-provision time — never stored in plaintext on the row.
|
||||
credSecretId String?
|
||||
credSecretKey String?
|
||||
ownerId String
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
cred Secret? @relation(fields: [credSecretId], references: [id])
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
tasks Task[]
|
||||
|
||||
@@unique([name, projectId])
|
||||
@@index([projectId])
|
||||
}
|
||||
```
|
||||
|
||||
### AgentProject
|
||||
|
||||
Mirrors `ProjectServer` (`schema.prisma`): explicit join model with its own cuid `id`, `createdAt`,
|
||||
cascade on both sides, `@@unique([a, b])`. Indexes on both columns follow `PersonalityPrompt`,
|
||||
because both reverse lookups are needed ("agents of project", "projects of agent").
|
||||
|
||||
```prisma
|
||||
model AgentProject {
|
||||
id String @id @default(cuid())
|
||||
agentId String
|
||||
projectId String
|
||||
// Per-project persona role. TEXT, not an enum, so adding a role later is a
|
||||
// non-migration change (same reasoning as ResourceProposal.resourceType).
|
||||
// v1 values: 'developer' | 'qa' | 'security' | 'sre'.
|
||||
role String @default("developer")
|
||||
// Optional per-membership brain override. Present so the open question in
|
||||
// docs/agentic-teams.md §15 Q1 can be answered either way without a migration.
|
||||
llmId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
llm Llm? @relation(fields: [llmId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([agentId, projectId])
|
||||
@@index([agentId])
|
||||
@@index([projectId])
|
||||
}
|
||||
```
|
||||
|
||||
`Agent.projectId` is **retained** and written in sync with the junction for one release, so
|
||||
existing `prepareContext` / `findByProjectId` paths keep working. A follow-up removes it.
|
||||
|
||||
### AgentSession
|
||||
|
||||
```prisma
|
||||
model AgentSession {
|
||||
id String @id @default(cuid())
|
||||
agentId String
|
||||
projectId String
|
||||
taskId String?
|
||||
// Orchestrator handle — pod name on k8s, container id on docker.
|
||||
containerId String?
|
||||
state SessionState @default(pending)
|
||||
image String
|
||||
// Reaped when now() > expiresAt. Swept by the same style of GC loop as the
|
||||
// inference-task sweep in main.ts.
|
||||
expiresAt DateTime
|
||||
lastError String? @db.Text
|
||||
ownerId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
endedAt DateTime?
|
||||
|
||||
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([state])
|
||||
@@index([taskId])
|
||||
@@index([agentId])
|
||||
}
|
||||
|
||||
enum SessionState {
|
||||
pending
|
||||
provisioning
|
||||
running
|
||||
ended
|
||||
failed
|
||||
}
|
||||
```
|
||||
|
||||
### Back-relations on existing models
|
||||
|
||||
Prisma requires both sides of every relation. The blocks above show only the owning side; the
|
||||
following fields must be added to existing models or `prisma generate` fails:
|
||||
|
||||
| Model | Fields to add |
|
||||
|---|---|
|
||||
| `Project` | `epics Epic[]`, `codeRepos CodeRepo[]`, `agentMemberships AgentProject[]`, `sessions AgentSession[]` |
|
||||
| `User` | `ownedEpics Epic[]`, `ownedEpicTasks EpicTask[]`, `ownedTasks Task[]`, `ownedRepos CodeRepo[]`, `ownedSessions AgentSession[]` |
|
||||
| `Agent` | `projectMemberships AgentProject[]`, `assignedTasks Task[]`, `verdicts TaskVerdict[]`, `sessions AgentSession[]` |
|
||||
| `Llm` | `agentProjects AgentProject[]` |
|
||||
| `Secret` | `codeRepos CodeRepo[]` |
|
||||
|
||||
`Agent.projectId` and its `project Project?` relation stay as-is during the transition release, so
|
||||
`Project` carries both `agents Agent[]` (existing) and `agentMemberships AgentProject[]` (new).
|
||||
|
||||
### Test helper
|
||||
|
||||
`src/db/tests/helpers.ts` `clearAllTables` must gain `deleteMany` calls for every new model —
|
||||
globals with nullable foreign keys do not cascade.
|
||||
|
||||
## API Changes
|
||||
|
||||
All routes sit under `/api/v1`, follow the existing Fastify + zod-in-the-service pattern, and
|
||||
resolve `project` / `agent` name strings to ids at the route layer (as `routes/skills.ts` does).
|
||||
|
||||
### New endpoints
|
||||
|
||||
```
|
||||
# Vision layer
|
||||
GET|POST /api/v1/epics ?project=<name>
|
||||
GET|PUT|DELETE /api/v1/epics/:id
|
||||
GET /api/v1/epics/:id/roadmap → epictask graph + per-node completeness
|
||||
POST /api/v1/epics/:id/tradeoffs → append an accepted-tradeoff record
|
||||
|
||||
GET|POST /api/v1/epictasks ?epic=<name>&project=<name>
|
||||
GET|PUT|DELETE /api/v1/epictasks/:id
|
||||
POST|DELETE /api/v1/epictasks/:id/edges → { toId } add/remove a dependency edge
|
||||
|
||||
# Work items
|
||||
GET|POST /api/v1/tasks ?project=&epictask=&state=&assignee=
|
||||
GET|PUT|DELETE /api/v1/tasks/:id
|
||||
POST /api/v1/tasks/:id/transition → { toState, note? }
|
||||
POST /api/v1/tasks/:id/verdict → record a QA/Security/SRE verdict
|
||||
POST /api/v1/tasks/:id/accept → { note? } human accept gate → merge
|
||||
POST /api/v1/tasks/:id/reject → { note } → back to in_progress
|
||||
GET /api/v1/tasks/:id/events
|
||||
|
||||
# Repositories
|
||||
GET|POST /api/v1/repos ?project=<name>
|
||||
GET|PUT|DELETE /api/v1/repos/:id
|
||||
|
||||
# Agent membership
|
||||
GET /api/v1/agents/:name/projects
|
||||
POST|DELETE /api/v1/agents/:name/projects/:project → { role?, llm? }
|
||||
GET /api/v1/projects/:name/agents
|
||||
|
||||
# Sessions & exec
|
||||
GET|POST /api/v1/sessions ?project=&agent=&task=
|
||||
GET|DELETE /api/v1/sessions/:id
|
||||
POST /api/v1/sessions/:id/exec → one-shot exec, returns { exitCode, stdout, stderr }
|
||||
GET /api/v1/sessions/:id/attach → WebSocket, bidirectional stdio
|
||||
|
||||
# Git provider webhook
|
||||
POST /api/v1/webhooks/git/:provider → post-merge failure reopens the task
|
||||
```
|
||||
|
||||
### Changed endpoints
|
||||
|
||||
`/api/v1/inference-tasks/*` is unchanged in shape. Only its RBAC resource name changes (see
|
||||
below).
|
||||
|
||||
## RBAC Changes
|
||||
|
||||
### The `tasks` namespace split
|
||||
|
||||
`src/mcpd/src/validation/rbac-definition.schema.ts` currently declares `tasks` in
|
||||
`RBAC_RESOURCES` and aliases `task`, `inference-task`, and `inference-tasks` all onto it — with
|
||||
the comment "the only 'task' we have is inference for now". That is no longer true.
|
||||
|
||||
Required changes:
|
||||
|
||||
1. `RBAC_RESOURCES` gains `epics`, `epictasks`, `tasks` (now meaning work items), `repos`,
|
||||
`sessions`, and `inference-tasks`.
|
||||
2. `RESOURCE_ALIASES` maps `task → tasks`, `epic → epics`, `epictask → epictasks`,
|
||||
`et → epictasks`, `repo → repos`, `repository → repos`, `repositories → repos`,
|
||||
`session → sessions`, and `inference-task → inference-tasks`. The entry
|
||||
`'inference-tasks': 'tasks'` is **removed**.
|
||||
3. `mapUrlToPermission` in `src/mcpd/src/main.ts` gains `epics`, `epictasks`, `tasks`, `repos`,
|
||||
`sessions`, and maps `/api/v1/inference-tasks` to `inference-tasks`. Non-CRUD verbs need
|
||||
explicit cases above the map: `POST /tasks/:id/accept|reject|transition` → `run:tasks:<name>`,
|
||||
`POST /sessions/:id/exec` and `GET /sessions/:id/attach` → `run:sessions:<name>`.
|
||||
4. `nameResolvers` (`main.ts`) gains `epics`, `epictasks`, `tasks`, `repos`, `sessions` so
|
||||
name-scoped grants match when the URL carries a cuid.
|
||||
|
||||
### One-shot binding backfill
|
||||
|
||||
`ResourceBindingSchema` transforms `resource` through `normalizeResource` **at parse time** and
|
||||
stores the normalized string, so existing `RbacDefinition.roleBindings` rows already contain the
|
||||
literal `"tasks"` meaning *inference*. After the split, those grants would silently become
|
||||
work-item grants.
|
||||
|
||||
A one-shot startup migration must rewrite `resource: "tasks"` → `"inference-tasks"` across all
|
||||
`RbacDefinition` rows. Model the walk on `migrateAdminRole` in `src/mcpd/src/main.ts` (it iterates
|
||||
`rbacRepo.findAll()` and rewrites `roleBindings` in place).
|
||||
|
||||
It must be **guarded so it runs exactly once** — a blanket rewrite on every boot would clobber
|
||||
legitimate new work-item grants. Use the existing system-settings pattern: a boolean key in the
|
||||
`mcpctl-system-settings` Secret (`SYSTEM_SETTINGS_SECRET` in
|
||||
`src/mcpd/src/bootstrap/system-settings.ts`), read before the walk and written after. Wildcard
|
||||
bindings (`resource: '*'`) are untouched — they already cover both.
|
||||
|
||||
Document the rename in `docs/inference-tasks.md`.
|
||||
|
||||
## CLI Changes
|
||||
|
||||
Two project rules govern every resource added here:
|
||||
|
||||
- Everything settable via `apply -f` **must** also be a `create` flag.
|
||||
- `mcpctl get RESOURCE NAME -o yaml` **must** round-trip through `mcpctl apply -f`.
|
||||
|
||||
### New commands
|
||||
|
||||
```
|
||||
mcpctl get epics|epictasks|tasks|repos|sessions [--project NAME]
|
||||
mcpctl describe epic|epictask|task|repo|session NAME
|
||||
mcpctl create epic|epictask|task|repo ...
|
||||
mcpctl edit epic|epictask|task|repo NAME
|
||||
mcpctl delete epic|epictask|task|repo NAME
|
||||
|
||||
mcpctl describe epic NAME # roadmap: epictask graph + per-node completeness
|
||||
mcpctl tasks accept NAME [--note ..] # human accept gate → merge via provider API
|
||||
mcpctl tasks reject NAME --note .. # → back to in_progress
|
||||
mcpctl exec --project NAME AGENT -- bash
|
||||
```
|
||||
|
||||
`mcpctl exec` semantics (design doc §10): exec drops the human into the **working environment** —
|
||||
the cloned repos, branches, tools, and deps. It does **not** attach to the agent's reasoning loop.
|
||||
The agent remains a separate process the human can still talk to.
|
||||
|
||||
### Files to touch
|
||||
|
||||
| Concern | File |
|
||||
|---|---|
|
||||
| Resource aliases | `src/cli/src/commands/shared.ts` `RESOURCE_ALIASES` |
|
||||
| `get` columns + yaml kind | `src/cli/src/commands/get.ts` — row interfaces, `getColumnsForResource`, `RESOURCE_KIND`, `toApplyDocs` strip blocks |
|
||||
| `describe` formatters | `src/cli/src/commands/describe.ts` — clean sections and tables, not key-value dumps |
|
||||
| `create` subcommands | `src/cli/src/commands/create.ts` |
|
||||
| `apply -f` | `src/cli/src/commands/apply.ts` — spec schemas, `ApplyConfigSchema` keys, `KIND_TO_RESOURCE`, dry-run counts, apply loops |
|
||||
| `edit` | `src/cli/src/commands/edit.ts` `validResources` |
|
||||
| Scoped fetch | `src/cli/src/index.ts` `fetchResource` / `fetchSingleResource` for `--project` scoping |
|
||||
| Completions | `scripts/generate-completions.ts` `CANONICAL_RESOURCES` + `ALIAS_ENTRIES`, then `pnpm completions:generate` |
|
||||
|
||||
Completions are generated, never hand-edited; `src/cli/tests/completions.test.ts` runs the
|
||||
generator with `--check` and fails on drift.
|
||||
|
||||
## Execution Substrate Changes
|
||||
|
||||
These are the primitives the design document assumes but mcpctl does not have. They gate step 2.
|
||||
|
||||
### 1. Ephemeral pods
|
||||
|
||||
`ContainerSpec` in `src/mcpd/src/services/orchestrator.ts` has only image, name, command, env,
|
||||
ports, limits, labels, and network. `generatePodSpec` in
|
||||
`src/mcpd/src/services/k8s/manifest-generator.ts` hardcodes `restartPolicy: 'Always'`, and no code
|
||||
anywhere uses the Kubernetes Job API.
|
||||
|
||||
`ContainerSpec` must gain:
|
||||
|
||||
- `restartPolicy?: 'Always' | 'Never' | 'OnFailure'`
|
||||
- `workdir?: string`
|
||||
- `volumes?: Array<{ name: string; mountPath: string; kind: 'emptyDir' }>` — the session
|
||||
workspace
|
||||
- `initContainers?: Array<{ name; image; command; env?; volumeMounts? }>` — the repo-clone step
|
||||
|
||||
Both backends implement them: `KubernetesOrchestrator` and `DockerContainerManager`.
|
||||
|
||||
### 2. Session provisioning
|
||||
|
||||
A `SessionService` that: creates the `AgentSession` row; resolves the project's `CodeRepo` list and
|
||||
their credentials through `SecretService` (resolution failure is fatal for the attempt, matching
|
||||
`InstanceService.attemptStart`); builds a spec with an init container that clones each repo to its
|
||||
`path` under the workspace volume and checks out the task branch; starts the pod; and transitions
|
||||
the row to `running`.
|
||||
|
||||
Reaping runs as a GC sweep in the same style as the existing inference-task sweep in
|
||||
`src/mcpd/src/main.ts`: sessions past `expiresAt` are stopped, removed, and marked `ended`. Note
|
||||
these loops assume a single mcpd replica — there is no leader election, and this inherits that.
|
||||
|
||||
### 3. Exec surface
|
||||
|
||||
The hard part already exists. `McpOrchestrator` declares `execInContainer`, `execInteractive`, and
|
||||
`attachInteractive`; `KubernetesOrchestrator` implements the latter two over `k8s.Exec` and
|
||||
`k8s.Attach`, and `src/mcpd/src/services/transport/persistent-stdio.ts` uses them in production
|
||||
today. This work is an **API and CLI surface over a proven primitive**: a one-shot exec route, a
|
||||
WebSocket attach route with terminal resize and proper exit-code propagation, and the
|
||||
`mcpctl exec` client.
|
||||
|
||||
### 4. Git provider client
|
||||
|
||||
No Gitea or GitHub API client, PR creation, or webhook handling exists in product code. PRs are
|
||||
made today by the root-level `pr.sh` (curl against the Gitea API) or by an agent calling the
|
||||
`gitea-mcp-server` MCP tool.
|
||||
|
||||
A `GitProvider` interface in mcpd with a Gitea driver: `createPullRequest`, `getPullRequest`,
|
||||
`getCombinedStatus`, `mergePullRequest`. Credentials resolve from the `CodeRepo`'s Secret.
|
||||
`pr.sh` is the reference for the endpoint shapes (`POST /api/v1/repos/:owner/:repo/pulls`).
|
||||
A webhook receiver validates the provider signature and reopens the task on post-merge failure.
|
||||
|
||||
## AgentRuntime Interface
|
||||
|
||||
mcpctl builds an *interface*, not a runtime. `ChatService` is not a candidate — its tool loop is
|
||||
capped at `MAX_ITERATIONS = 12` (clamped 1..50), which is a soft-DoS guard appropriate for chat
|
||||
and far too small for a coding task.
|
||||
|
||||
```ts
|
||||
export interface AgentRuntime {
|
||||
startSession(opts: RuntimeSessionOpts): Promise<RuntimeSession>;
|
||||
send(sessionId: string, message: string): Promise<void>;
|
||||
status(sessionId: string): Promise<RuntimeStatus>;
|
||||
result(sessionId: string): Promise<RuntimeResult>; // branch, head commit, summary
|
||||
endSession(sessionId: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
Implementation #1 runs a battle-tested headless coding agent inside the session pod. It already
|
||||
solves per-tool permission approval, git and branch mechanics, subagents, streaming, and MCP
|
||||
client behaviour — all things the design document explicitly declines to rebuild. The persona is
|
||||
delivered as prompt files in the container, which suits a file-aware coding runtime.
|
||||
|
||||
A second adapter is explicitly deferred; the interface exists so it stays possible.
|
||||
|
||||
## Task Lifecycle
|
||||
|
||||
```
|
||||
todo → in_progress → building → self_testing
|
||||
→ qa_review (feedback → in_progress)
|
||||
→ security_review (feedback → in_progress)
|
||||
→ sre_review (feedback → in_progress)
|
||||
→ done → awaiting_review
|
||||
accept → merged
|
||||
reject → in_progress
|
||||
merged --post-merge failure (webhook)--> reopened
|
||||
```
|
||||
|
||||
Verification effort scales with `Task.risk`: `trivial` work may pass on a light self-check;
|
||||
`high` engages all three review stages. The stages decide their own depth.
|
||||
|
||||
Transitions are guarded by a table of legal `(from, to)` pairs and applied as **compare-and-swap**
|
||||
— `updateMany` with the expected `state` in the `where` clause, then check the affected-row count.
|
||||
This is the same idiom `InferenceTaskRepository` uses so two racing claimants cannot both win.
|
||||
Every transition appends a `TaskEvent`.
|
||||
|
||||
The human approval gate is **not** a per-tool tri-state. It is a task-level yes/no plus a comment
|
||||
on the resolved outcome, and accepting it fires the merge. The human should not have to go to git.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit — `src/db/tests/`
|
||||
- Schema tests per model (pattern: `skill-schema.test.ts`): create, unique constraints, cascade
|
||||
behaviour on project/epic/task delete, `AgentProject` uniqueness.
|
||||
- `clearAllTables` in `helpers.ts` covers every new model.
|
||||
|
||||
### Unit — `src/mcpd/tests/`
|
||||
- Service tests: Epic done-criteria roll-up; EpicTask edge cycle rejection; task transition table
|
||||
including every illegal pair; compare-and-swap under concurrent transition.
|
||||
- Route tests per resource (pattern: `prompt-routes.test.ts`, `agent-routes.test.ts`).
|
||||
- RBAC: the `tasks` → `inference-tasks` backfill runs once and is idempotent on a second boot;
|
||||
a pre-existing `view:tasks` grant still authorizes `/api/v1/inference-tasks` afterwards and does
|
||||
**not** authorize `/api/v1/tasks`; name-scoped grants resolve for all new resources.
|
||||
- Orchestrator: `generatePodSpec` emits `restartPolicy`, workdir, volumes, and init containers.
|
||||
- Git provider: Gitea driver against a mocked HTTP server.
|
||||
|
||||
### Unit — `src/cli/tests/`
|
||||
- `get -o yaml` → `apply -f` round-trip for every new resource.
|
||||
- `create` flag coverage: every field settable in `apply` has a matching flag.
|
||||
- `completions.test.ts` assertions for the new resource words.
|
||||
|
||||
### Smoke — `src/mcplocal/tests/smoke/`
|
||||
Every feature ships smoke tests. Follow the CRUD pattern in `personality.smoke.test.ts`: a
|
||||
`Date.now().toString(36)` name suffix, a `healthz()` preflight that **skips rather than fails**
|
||||
when the daemon is unreachable, `mcpctl --direct` shelling, and explicit `afterAll` cleanup.
|
||||
|
||||
- `epic.smoke.test.ts` — Epic → EpicTask → Task create/list/describe/delete against live mcpd.
|
||||
- `repo.smoke.test.ts` — CodeRepo CRUD, credential reference resolves.
|
||||
- `session.smoke.test.ts` — provision a session pod against the real cluster, confirm the repo is
|
||||
cloned, exec a command, confirm the pod is reaped.
|
||||
- `task-lifecycle.smoke.test.ts` — drive a task through the full transition chain including a
|
||||
rejected accept-gate bounce.
|
||||
|
||||
When a smoke test fails, fix the environment (deploy, backend, RBAC) — never weaken the assertion.
|
||||
|
||||
## Deferred
|
||||
|
||||
Named here so the boundary is explicit. Each gets a single placeholder task with no detail, to be
|
||||
expanded once the corresponding open questions in `docs/agentic-teams.md` §15 are closed.
|
||||
|
||||
- **Tier-2 vision critic and Epic-comparison.** A review-only, non-executing design/product
|
||||
persona that judges the assembled result against the Epic in its native medium, producing
|
||||
reopen / new-task / `exec` outcomes. Blocked on §15 Q13 (does this need render/screenshot
|
||||
infrastructure, or can a design-aware persona judge from a structured artifact?).
|
||||
- **Collaboration modes.** On-demand async standup (`mcpctl standup --project NAME`) and `exec`
|
||||
as a workflow rather than a command. Blocked on §15 Q4 and Q5.
|
||||
- **Confidence signal and escalation dashboard.** Evidence-anchored confidence (retrieve K similar
|
||||
past tasks with their *outcomes*, ask how confident we are this resolves cleanly), thresholds
|
||||
driving autonomy, and a single cross-project human decision queue. Blocked on §15 Q12
|
||||
(web / TUI / CLI?). Retrieval quality is the crux — weak retrieval quietly poisons the model.
|
||||
- **Memory and the coverage ledger.** Global per-agent memory plus per-project knowledge, and a
|
||||
commit-pinned coverage ledger enabling delta-verification. Non-negotiable prerequisite:
|
||||
invalidation when a covered region changes, or the ledger becomes false confidence. Keep the
|
||||
trust boundary — the ledger is platform-verified machine fact; agent prose notes are opinion and
|
||||
never the gate.
|
||||
- **Script artifacts and the baseline scorecard.** Reusable executable `script` artifacts at task,
|
||||
project, and agent scope, plus a provisioned baseline-rules scorecard. Blocked on §15 Q15 and
|
||||
Q16. Watch the incentive integrity risk: reward *reused* tools, not tool proliferation.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
**Phase A — foundation (blocks everything)**
|
||||
1. RBAC `tasks` namespace split, alias repoint, and the one-shot guarded binding backfill.
|
||||
2. Prisma models Epic, EpicTask, EpicTaskEdge, Task, TaskVerdict, TaskEvent, CodeRepo,
|
||||
AgentSession + enums + migration + `clearAllTables`.
|
||||
3. `AgentProject` junction, with `Agent.projectId` kept in sync for one release.
|
||||
|
||||
**Phase B — mcpd CRUD**
|
||||
4. Epic resource: repository, service, zod validation, routes, `main.ts` wiring, revision opt-in.
|
||||
5. EpicTask resource plus the edge graph and cycle rejection.
|
||||
6. Task resource plus the transition table and compare-and-swap transitions.
|
||||
7. CodeRepo resource with credential resolution.
|
||||
8. RBAC wiring for all new resources: `mapUrlToPermission`, `nameResolvers`, non-CRUD verb cases.
|
||||
9. Backup and YAML kinds for the new resources — `BACKUP_KINDS`, `APPLY_ORDER`, `kindMap`, the
|
||||
export loop, `serializeResource`, and `main.ts` `kindFromSegment` / `importResource` /
|
||||
`deleteResource`. Fix the pre-existing gap where `skill` is missing from the latter two.
|
||||
|
||||
**Phase C — CLI**
|
||||
10. `get` columns and `describe` formatters for all new resources.
|
||||
11. `create`, `apply -f`, `edit`, `delete`, with the yaml round-trip test.
|
||||
12. Shell completions and their guard test.
|
||||
13. Roadmap view: `describe epic` rendering the epictask graph with per-node completeness.
|
||||
|
||||
**Phase D — execution substrate**
|
||||
14. Extend `ContainerSpec` and both orchestrator backends with restart policy, workdir, volumes,
|
||||
and init containers.
|
||||
15. `SessionService`: provisioning, repo-clone init container, credential resolution, TTL and GC.
|
||||
16. `AgentRuntime` interface plus adapter #1 running a headless coding agent in-pod.
|
||||
17. Exec surface: one-shot exec route, WebSocket attach, and `mcpctl exec`.
|
||||
|
||||
**Phase E — lifecycle and PR**
|
||||
18. Task lifecycle state machine, transition API, and `TaskEvent` log.
|
||||
19. `GitProvider` interface plus the Gitea driver.
|
||||
20. PR linkage and the human accept gate firing merge through the provider API.
|
||||
21. QA / Security / SRE stage verdicts, risk-scaled, pinned to a commit.
|
||||
22. Post-merge failure webhook reopening the task.
|
||||
|
||||
**Phase F — cross-cutting**
|
||||
23. Documentation: `docs/epics.md`, `docs/tasks.md`, `docs/repos.md`, `docs/sessions.md`, the
|
||||
README resources table, and the CLAUDE.md resource cheatsheet.
|
||||
24. Smoke tests for every new resource plus the session and lifecycle flows.
|
||||
|
||||
**Phase G — deferred placeholders**
|
||||
25. Tier-2 vision critic and Epic-comparison loop.
|
||||
26. Collaboration modes: async standup and `exec` joint sessions.
|
||||
27. Confidence signal and escalation dashboard.
|
||||
28. Per-agent memory and the coverage ledger.
|
||||
29. Script artifacts and the baseline scorecard.
|
||||
Reference in New Issue
Block a user