809 lines
37 KiB
Markdown
809 lines
37 KiB
Markdown
|
|
# 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.
|