docs: add implementation plans for plan CRUD and issue dispatch
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
3cd21f2fc1
commit
3b8f17d8fd
3 changed files with 574 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
.core/
|
.core/
|
||||||
.idea/
|
.idea/
|
||||||
|
core-mcp
|
||||||
|
|
|
||||||
281
docs/plans/2026-03-15-issue-driven-dispatch.md
Normal file
281
docs/plans/2026-03-15-issue-driven-dispatch.md
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
# Issue-Driven Dispatch Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Agents claim Forge issues, work in sandboxed workspaces with feature branches, and create PRs when done. Assignment = lock (no two agents work the same issue).
|
||||||
|
|
||||||
|
**Architecture:** New MCP tool `agentic_dispatch_issue` takes an issue number + repo, assigns the issue to the agent (lock), preps a workspace with the issue body as TODO.md, creates a feature branch `agent/issue-{num}-{slug}`, and dispatches. On completion, a `agentic_pr` tool creates a PR via the Forge API linking back to the issue.
|
||||||
|
|
||||||
|
**Tech Stack:** Go, MCP SDK, Forge/Gitea API (go-scm), git
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Existing Infrastructure
|
||||||
|
|
||||||
|
Already built:
|
||||||
|
- `agentic_dispatch` — preps workspace + spawns agent (dispatch.go)
|
||||||
|
- `agentic_scan` — finds issues with actionable labels (scan.go)
|
||||||
|
- `prepWorkspace()` — clones repo, creates feature branch, writes context files
|
||||||
|
- `generateTodo()` — fetches issue from Forge API, writes TODO.md
|
||||||
|
- PrepSubsystem has `forgeURL`, `forgeToken`, `client` fields
|
||||||
|
|
||||||
|
The issue flow just wires scan → dispatch together with assignment as the lock.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Action | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `pkg/mcp/agentic/issue.go` | Create | `agentic_dispatch_issue` + `agentic_pr` tools |
|
||||||
|
| `pkg/mcp/agentic/prep.go` | Modify | Register new tools |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Issue Dispatch Tool
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `pkg/mcp/agentic/issue.go`
|
||||||
|
- Modify: `pkg/mcp/agentic/prep.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create issue.go with input/output types**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// issue.go
|
||||||
|
package agentic
|
||||||
|
|
||||||
|
// IssueDispatchInput for agentic_dispatch_issue
|
||||||
|
type IssueDispatchInput struct {
|
||||||
|
Repo string `json:"repo"` // Target repo (e.g. "go-io")
|
||||||
|
Org string `json:"org,omitempty"` // Forge org (default "core")
|
||||||
|
Issue int `json:"issue"` // Forge issue number
|
||||||
|
Agent string `json:"agent,omitempty"` // "gemini", "codex", "claude" (default "claude")
|
||||||
|
Template string `json:"template,omitempty"` // "conventions", "security", "coding" (default "coding")
|
||||||
|
DryRun bool `json:"dry_run,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRInput for agentic_pr
|
||||||
|
type PRInput struct {
|
||||||
|
Workspace string `json:"workspace"` // Workspace name
|
||||||
|
Title string `json:"title,omitempty"` // PR title (default: from issue)
|
||||||
|
Body string `json:"body,omitempty"` // PR body (default: auto-generated)
|
||||||
|
Base string `json:"base,omitempty"` // Base branch (default: "main")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement agentic_dispatch_issue**
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Fetch issue from Forge API to validate it exists
|
||||||
|
2. Check issue is not already assigned (if assigned, return error — it's locked)
|
||||||
|
3. Assign issue to agent (POST /api/v1/repos/{org}/{repo}/issues/{num} with assignee)
|
||||||
|
4. Add "in-progress" label
|
||||||
|
5. Call existing `dispatch()` with the issue number (it already handles TODO.md generation)
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) dispatchIssue(ctx context.Context, req *mcp.CallToolRequest, input IssueDispatchInput) (*mcp.CallToolResult, DispatchOutput, error) {
|
||||||
|
if input.Issue == 0 {
|
||||||
|
return nil, DispatchOutput{}, fmt.Errorf("issue number is required")
|
||||||
|
}
|
||||||
|
if input.Repo == "" {
|
||||||
|
return nil, DispatchOutput{}, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
if input.Org == "" {
|
||||||
|
input.Org = "core"
|
||||||
|
}
|
||||||
|
if input.Agent == "" {
|
||||||
|
input.Agent = "claude"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Fetch issue to validate and check assignment
|
||||||
|
issueURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, input.Org, input.Repo, input.Issue)
|
||||||
|
issueReq, _ := http.NewRequestWithContext(ctx, "GET", issueURL, nil)
|
||||||
|
issueReq.Header.Set("Authorization", "token "+s.forgeToken)
|
||||||
|
|
||||||
|
resp, err := s.client.Do(issueReq)
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
return nil, DispatchOutput{}, fmt.Errorf("issue %d not found in %s/%s", input.Issue, input.Org, input.Repo)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var issue struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Assignee *struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
} `json:"assignee"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&issue)
|
||||||
|
|
||||||
|
if issue.State != "open" {
|
||||||
|
return nil, DispatchOutput{}, fmt.Errorf("issue %d is %s, not open", input.Issue, issue.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check lock (assignment)
|
||||||
|
if issue.Assignee != nil && issue.Assignee.Login != "" {
|
||||||
|
return nil, DispatchOutput{}, fmt.Errorf("issue %d already assigned to %s", input.Issue, issue.Assignee.Login)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Assign to agent (lock)
|
||||||
|
if !input.DryRun && s.forgeToken != "" {
|
||||||
|
assignBody, _ := json.Marshal(map[string]any{"assignees": []string{input.Agent}})
|
||||||
|
assignReq, _ := http.NewRequestWithContext(ctx, "PATCH", issueURL, bytes.NewReader(assignBody))
|
||||||
|
assignReq.Header.Set("Authorization", "token "+s.forgeToken)
|
||||||
|
assignReq.Header.Set("Content-Type", "application/json")
|
||||||
|
s.client.Do(assignReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Dispatch via existing dispatch()
|
||||||
|
return s.dispatch(ctx, req, DispatchInput{
|
||||||
|
Repo: input.Repo,
|
||||||
|
Org: input.Org,
|
||||||
|
Issue: input.Issue,
|
||||||
|
Task: issue.Title,
|
||||||
|
Agent: input.Agent,
|
||||||
|
Template: input.Template,
|
||||||
|
DryRun: input.DryRun,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement agentic_pr**
|
||||||
|
|
||||||
|
Creates a PR from the agent's feature branch back to main, referencing the issue.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, input PRInput) (*mcp.CallToolResult, map[string]any, error) {
|
||||||
|
if input.Workspace == "" {
|
||||||
|
return nil, nil, fmt.Errorf("workspace is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
wsDir := filepath.Join(home, "Code", "host-uk", "core", ".core", "workspace", input.Workspace)
|
||||||
|
srcDir := filepath.Join(wsDir, "src")
|
||||||
|
|
||||||
|
// Read status to get repo info
|
||||||
|
st, err := readStatus(wsDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("no status.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current branch name
|
||||||
|
branchCmd := exec.CommandContext(ctx, "git", "branch", "--show-current")
|
||||||
|
branchCmd.Dir = srcDir
|
||||||
|
branchOut, err := branchCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to get branch: %w", err)
|
||||||
|
}
|
||||||
|
branch := strings.TrimSpace(string(branchOut))
|
||||||
|
|
||||||
|
// Push branch to forge
|
||||||
|
pushCmd := exec.CommandContext(ctx, "git", "push", "origin", branch)
|
||||||
|
pushCmd.Dir = srcDir
|
||||||
|
if out, err := pushCmd.CombinedOutput(); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("push failed: %s", string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine PR title and body
|
||||||
|
title := input.Title
|
||||||
|
if title == "" {
|
||||||
|
title = st.Task
|
||||||
|
}
|
||||||
|
base := input.Base
|
||||||
|
if base == "" {
|
||||||
|
base = "main"
|
||||||
|
}
|
||||||
|
body := input.Body
|
||||||
|
if body == "" {
|
||||||
|
body = fmt.Sprintf("Automated PR from agent workspace `%s`.\n\nTask: %s", input.Workspace, st.Task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PR via Forge API
|
||||||
|
org := "core" // TODO: extract from status
|
||||||
|
prURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls", s.forgeURL, org, st.Repo)
|
||||||
|
prBody, _ := json.Marshal(map[string]any{
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
"head": branch,
|
||||||
|
"base": base,
|
||||||
|
})
|
||||||
|
|
||||||
|
prReq, _ := http.NewRequestWithContext(ctx, "POST", prURL, bytes.NewReader(prBody))
|
||||||
|
prReq.Header.Set("Authorization", "token "+s.forgeToken)
|
||||||
|
prReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := s.client.Do(prReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("PR creation failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var prResult map[string]any
|
||||||
|
json.NewDecoder(resp.Body).Decode(&prResult)
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, nil, fmt.Errorf("PR creation returned %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, prResult, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Register tools in prep.go**
|
||||||
|
|
||||||
|
Add to `RegisterTools()`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
s.registerIssueTools(server)
|
||||||
|
```
|
||||||
|
|
||||||
|
Registration:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) registerIssueTools(server *mcp.Server) {
|
||||||
|
mcp.AddTool(server, &mcp.Tool{
|
||||||
|
Name: "agentic_dispatch_issue",
|
||||||
|
Description: "Dispatch an agent to work on a Forge issue. Assigns the issue (lock), preps workspace with issue body as TODO.md, creates feature branch, spawns agent.",
|
||||||
|
}, s.dispatchIssue)
|
||||||
|
|
||||||
|
mcp.AddTool(server, &mcp.Tool{
|
||||||
|
Name: "agentic_pr",
|
||||||
|
Description: "Create a PR from an agent workspace. Pushes the feature branch and creates a pull request on Forge linking to the original issue.",
|
||||||
|
}, s.createPR)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify compilation**
|
||||||
|
|
||||||
|
Run: `go vet ./pkg/mcp/agentic/`
|
||||||
|
Expected: clean
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pkg/mcp/agentic/issue.go pkg/mcp/agentic/prep.go
|
||||||
|
git commit -m "feat(agentic): issue-driven dispatch — claim, branch, PR
|
||||||
|
|
||||||
|
New MCP tools:
|
||||||
|
- agentic_dispatch_issue: assigns issue (lock), preps workspace, dispatches
|
||||||
|
- agentic_pr: pushes branch, creates PR via Forge API
|
||||||
|
|
||||||
|
Assignment = lock — no two agents work the same issue.
|
||||||
|
|
||||||
|
Co-Authored-By: Virgil <virgil@lethean.io>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Total: 1 task, 6 steps**
|
||||||
|
|
||||||
|
After completion, the full issue lifecycle:
|
||||||
|
1. `agentic_scan` — find issues with actionable labels
|
||||||
|
2. `agentic_dispatch_issue` — claim issue (assign = lock), prep workspace, spawn agent
|
||||||
|
3. Agent works in sandboxed workspace with feature branch
|
||||||
|
4. Agent writes BLOCKED.md if stuck → `agentic_resume` to continue
|
||||||
|
5. `agentic_pr` — push branch, create PR referencing the issue
|
||||||
|
6. PR reviewed and merged
|
||||||
|
|
||||||
|
Community flow:
|
||||||
|
- Maintainer creates issue with `agentic` label
|
||||||
|
- Agent scans, claims, works, PRs
|
||||||
|
- Maintainer reviews and merges
|
||||||
292
docs/plans/2026-03-15-plan-crud-mcp.md
Normal file
292
docs/plans/2026-03-15-plan-crud-mcp.md
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
# Plan CRUD MCP Tools Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add MCP tools for plan lifecycle management — create plans from templates, track phase progress, record checkpoints. Agents can structure their work into plans with phases and tasks, enabling stop/ask/resume workflows.
|
||||||
|
|
||||||
|
**Architecture:** Thin HTTP wrappers calling the existing Laravel API at `api.lthn.sh/v1/plans/*`. Same pattern as `brain.NewDirect()` — reads `CORE_BRAIN_KEY` for auth, calls REST endpoints, returns structured results. No local storage needed (the API handles persistence).
|
||||||
|
|
||||||
|
**Tech Stack:** Go, MCP SDK, HTTP client, JSON
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Existing API Endpoints (Laravel — already built)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /v1/plans # List plans
|
||||||
|
GET /v1/plans/{slug} # Get plan detail
|
||||||
|
POST /v1/plans # Create plan
|
||||||
|
PATCH /v1/plans/{slug} # Update plan
|
||||||
|
PATCH /v1/plans/{slug}/phases/{phase} # Update phase
|
||||||
|
POST /v1/plans/{slug}/phases/{phase}/checkpoint # Add checkpoint
|
||||||
|
POST /v1/plans/{slug}/phases/{phase}/tasks/{idx}/toggle # Toggle task
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Action | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `pkg/mcp/agentic/plans.go` | Create | Plan CRUD MCP tools — create, status, list, checkpoint, update |
|
||||||
|
| `pkg/mcp/agentic/plans_test.go` | Create | Tests with mock HTTP server |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Plan MCP Tools
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `pkg/mcp/agentic/plans.go`
|
||||||
|
- Modify: `pkg/mcp/agentic/prep.go` (register new tools)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the plan tools**
|
||||||
|
|
||||||
|
Create `plans.go` with 4 MCP tools that wrap the Laravel API:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// plans.go
|
||||||
|
package agentic
|
||||||
|
|
||||||
|
// Tool: agentic_plan_create
|
||||||
|
// Creates a plan from a template slug with variable substitution.
|
||||||
|
// POST /v1/plans with { template: slug, variables: {}, title: "", activate: bool }
|
||||||
|
|
||||||
|
// Tool: agentic_plan_status
|
||||||
|
// Gets plan detail including phases, tasks, and progress.
|
||||||
|
// GET /v1/plans/{slug}
|
||||||
|
|
||||||
|
// Tool: agentic_plan_list
|
||||||
|
// Lists all plans, optionally filtered by status.
|
||||||
|
// GET /v1/plans?status={status}
|
||||||
|
|
||||||
|
// Tool: agentic_plan_checkpoint
|
||||||
|
// Records a checkpoint on a phase (notes, completion status).
|
||||||
|
// POST /v1/plans/{slug}/phases/{phase}/checkpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
Input/output types:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// PlanCreateInput for agentic_plan_create
|
||||||
|
type PlanCreateInput struct {
|
||||||
|
Template string `json:"template"` // Template slug: bug-fix, code-review, feature, refactor
|
||||||
|
Variables map[string]string `json:"variables,omitempty"` // Template variables
|
||||||
|
Title string `json:"title,omitempty"` // Override plan title
|
||||||
|
Activate bool `json:"activate,omitempty"` // Activate immediately (default: draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanStatusInput for agentic_plan_status
|
||||||
|
type PlanStatusInput struct {
|
||||||
|
Slug string `json:"slug"` // Plan slug
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanListInput for agentic_plan_list
|
||||||
|
type PlanListInput struct {
|
||||||
|
Status string `json:"status,omitempty"` // Filter: draft, active, completed, archived
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanCheckpointInput for agentic_plan_checkpoint
|
||||||
|
type PlanCheckpointInput struct {
|
||||||
|
Slug string `json:"slug"` // Plan slug
|
||||||
|
Phase int `json:"phase"` // Phase order number
|
||||||
|
Notes string `json:"notes,omitempty"` // Checkpoint notes
|
||||||
|
Done bool `json:"done,omitempty"` // Mark phase as completed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each tool calls `s.apiCall()` (reuse the pattern from PrepSubsystem which already has `brainURL`, `brainKey`, and `client`).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement agentic_plan_create**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) planCreate(ctx context.Context, _ *mcp.CallToolRequest, input PlanCreateInput) (*mcp.CallToolResult, map[string]any, error) {
|
||||||
|
if input.Template == "" {
|
||||||
|
return nil, nil, fmt.Errorf("template is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]any{
|
||||||
|
"template": input.Template,
|
||||||
|
"variables": input.Variables,
|
||||||
|
"activate": input.Activate,
|
||||||
|
}
|
||||||
|
if input.Title != "" {
|
||||||
|
body["title"] = input.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.planAPICall(ctx, "POST", "/v1/plans", body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("agentic_plan_create: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement agentic_plan_status**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) planStatus(ctx context.Context, _ *mcp.CallToolRequest, input PlanStatusInput) (*mcp.CallToolResult, map[string]any, error) {
|
||||||
|
if input.Slug == "" {
|
||||||
|
return nil, nil, fmt.Errorf("slug is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.planAPICall(ctx, "GET", "/v1/plans/"+input.Slug, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("agentic_plan_status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement agentic_plan_list**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) planList(ctx context.Context, _ *mcp.CallToolRequest, input PlanListInput) (*mcp.CallToolResult, map[string]any, error) {
|
||||||
|
path := "/v1/plans"
|
||||||
|
if input.Status != "" {
|
||||||
|
path += "?status=" + input.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.planAPICall(ctx, "GET", path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("agentic_plan_list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement agentic_plan_checkpoint**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) planCheckpoint(ctx context.Context, _ *mcp.CallToolRequest, input PlanCheckpointInput) (*mcp.CallToolResult, map[string]any, error) {
|
||||||
|
if input.Slug == "" {
|
||||||
|
return nil, nil, fmt.Errorf("slug is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := fmt.Sprintf("/v1/plans/%s/phases/%d/checkpoint", input.Slug, input.Phase)
|
||||||
|
body := map[string]any{
|
||||||
|
"notes": input.Notes,
|
||||||
|
}
|
||||||
|
if input.Done {
|
||||||
|
body["status"] = "completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.planAPICall(ctx, "POST", path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("agentic_plan_checkpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Add planAPICall helper**
|
||||||
|
|
||||||
|
Reuses `brainURL` and `brainKey` from PrepSubsystem (same API, same auth):
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) planAPICall(ctx context.Context, method, path string, body any) (map[string]any, error) {
|
||||||
|
if s.brainKey == "" {
|
||||||
|
return nil, fmt.Errorf("no API key (set CORE_BRAIN_KEY)")
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqBody goio.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, _ := json.Marshal(body)
|
||||||
|
reqBody = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, method, s.brainURL+path, reqBody)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+s.brainKey)
|
||||||
|
|
||||||
|
resp, err := s.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respData, _ := goio.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf("API %d: %s", resp.StatusCode, string(respData))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
json.Unmarshal(respData, &result)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Register tools in prep.go**
|
||||||
|
|
||||||
|
Add to `RegisterTools()`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
s.registerPlanTools(server)
|
||||||
|
```
|
||||||
|
|
||||||
|
And the registration function:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) {
|
||||||
|
mcp.AddTool(server, &mcp.Tool{
|
||||||
|
Name: "agentic_plan_create",
|
||||||
|
Description: "Create an agent work plan from a template (bug-fix, code-review, feature, refactor). Plans have phases with tasks and checkpoints.",
|
||||||
|
}, s.planCreate)
|
||||||
|
|
||||||
|
mcp.AddTool(server, &mcp.Tool{
|
||||||
|
Name: "agentic_plan_status",
|
||||||
|
Description: "Get plan detail including phases, tasks, progress, and checkpoints.",
|
||||||
|
}, s.planStatus)
|
||||||
|
|
||||||
|
mcp.AddTool(server, &mcp.Tool{
|
||||||
|
Name: "agentic_plan_list",
|
||||||
|
Description: "List all plans, optionally filtered by status (draft, active, completed, archived).",
|
||||||
|
}, s.planList)
|
||||||
|
|
||||||
|
mcp.AddTool(server, &mcp.Tool{
|
||||||
|
Name: "agentic_plan_checkpoint",
|
||||||
|
Description: "Record a checkpoint on a plan phase. Include notes about progress, decisions, or blockers.",
|
||||||
|
}, s.planCheckpoint)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: Verify compilation**
|
||||||
|
|
||||||
|
Run: `go vet ./pkg/mcp/agentic/`
|
||||||
|
Expected: clean
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pkg/mcp/agentic/plans.go pkg/mcp/agentic/prep.go
|
||||||
|
git commit -m "feat(agentic): plan CRUD MCP tools — create, status, list, checkpoint
|
||||||
|
|
||||||
|
Thin HTTP wrappers calling api.lthn.sh/v1/plans/* REST endpoints.
|
||||||
|
Same auth pattern as brain tools (CORE_BRAIN_KEY).
|
||||||
|
Templates: bug-fix, code-review, feature, refactor.
|
||||||
|
|
||||||
|
Co-Authored-By: Virgil <virgil@lethean.io>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Total: 1 task, 9 steps**
|
||||||
|
|
||||||
|
After completion, agents have 4 new MCP tools:
|
||||||
|
- `agentic_plan_create` — start structured work from a template
|
||||||
|
- `agentic_plan_status` — check progress
|
||||||
|
- `agentic_plan_list` — see all plans
|
||||||
|
- `agentic_plan_checkpoint` — record milestones
|
||||||
|
|
||||||
|
Combined with `agentic_status` + `agentic_resume`, this gives the full lifecycle:
|
||||||
|
1. Create plan → 2. Dispatch agent → 3. Agent works through phases → 4. Agent hits blocker → writes BLOCKED.md → 5. Reviewer answers → 6. Resume → 7. Agent checkpoints completion
|
||||||
|
|
||||||
|
Available templates (from PHP system):
|
||||||
|
- `bug-fix` — diagnose, fix, test, verify
|
||||||
|
- `code-review` — audit, report, fix, re-check
|
||||||
|
- `feature` — design, implement, test, document
|
||||||
|
- `refactor` — analyse, restructure, test, verify
|
||||||
Loading…
Add table
Reference in a new issue