nomadcode/services/core/internal/authoring/request.go
toki 6ab8dd8f63 feat: authoring bridge implementation and scheduler improvements
- Add authoring adapter for openai integration
- Move 02+01_iop_authoring_bridge to archive
- Update scheduler jobs and workflow lifecycle
- Enhance openai client and model definitions
2026-06-13 20:47:09 +09:00

131 lines
4.7 KiB
Go

package authoring
import (
"encoding/json"
"strings"
"github.com/nomadcode/nomadcode-core/internal/model"
"github.com/nomadcode/nomadcode-core/internal/storage"
"github.com/nomadcode/nomadcode-core/internal/workitempipeline"
)
// pipelinePayload is the actual task.Payload shape produced by
// workitem.BuildCreateTaskInput. "message" carries the Plane ticket body
// (comment > description > title), and "work_item" carries the provider
// identity map (provider, tenant, project, id, state_id, external_url).
// "prompt" and "instructions" are accepted as a generic override shape.
type pipelinePayload struct {
// pipeline shape (set by workitem.BuildCreateTaskInput)
Message string `json:"message"`
WorkItem map[string]string `json:"work_item"`
// generic override shape
Prompt string `json:"prompt"`
Instructions string `json:"instructions"`
}
// isAuthoringTask returns true when the task is a Plane-origin authoring task
// that should be routed through the IOP workspace authoring bridge. The criteria
// are: source=="plane" and task metadata contains a "checkout" key with a
// non-empty slot path.
func isAuthoringTask(task storage.Task) (workitempipeline.CheckoutMetadata, bool) {
if task.Source != "plane" {
return workitempipeline.CheckoutMetadata{}, false
}
if len(task.Metadata) == 0 || string(task.Metadata) == "null" {
return workitempipeline.CheckoutMetadata{}, false
}
var meta struct {
Checkout *workitempipeline.CheckoutMetadata `json:"checkout"`
}
if err := json.Unmarshal(task.Metadata, &meta); err != nil || meta.Checkout == nil {
return workitempipeline.CheckoutMetadata{}, false
}
if strings.TrimSpace(meta.Checkout.SlotPath) == "" {
return workitempipeline.CheckoutMetadata{}, false
}
return *meta.Checkout, true
}
// providerIdentity resolves the Plane provider and work item ID for a task.
// Priority: storage.Task.ExternalProvider/ExternalID (set by workitem pipeline)
// → work_item payload map → task.Source / task.ID fallback.
func providerIdentity(task storage.Task, payload pipelinePayload) (provider, workItemID string) {
provider = task.Source
workItemID = task.ID
if task.ExternalProvider != nil && *task.ExternalProvider != "" {
provider = *task.ExternalProvider
}
if task.ExternalID != nil && *task.ExternalID != "" {
workItemID = *task.ExternalID
return provider, workItemID
}
// fall back to work_item map in payload (also set by workitem pipeline)
if id := strings.TrimSpace(payload.WorkItem["id"]); id != "" {
workItemID = id
}
if p := strings.TrimSpace(payload.WorkItem["provider"]); p != "" {
provider = p
}
return provider, workItemID
}
// BuildAuthoringGenerateInput constructs a workspace-bound model.GenerateInput
// for Plane-origin authoring tasks. Returns false when the task does not qualify
// as an authoring task (non-Plane source or missing checkout metadata).
func BuildAuthoringGenerateInput(task storage.Task) (model.GenerateInput, bool) {
checkout, ok := isAuthoringTask(task)
if !ok {
return model.GenerateInput{}, false
}
// input priority: prompt override > pipeline message > task title
input := strings.TrimSpace(task.Title)
instructions := ""
var p pipelinePayload
if len(task.Payload) > 0 && string(task.Payload) != "null" && string(task.Payload) != "{}" {
_ = json.Unmarshal(task.Payload, &p)
}
if body := strings.TrimSpace(p.Message); body != "" {
input = body
}
if override := strings.TrimSpace(p.Prompt); override != "" {
input = override
}
instructions = strings.TrimSpace(p.Instructions)
if instructions == "" {
instructions = buildAuthoringInstructions(task, checkout)
}
provider, workItemID := providerIdentity(task, p)
return model.GenerateInput{
Input: input,
Instructions: instructions,
Metadata: map[string]string{
"task_id": task.ID,
"source": task.Source,
},
WorkspaceMetadata: &model.WorkspaceMetadata{
Path: checkout.SlotPath,
SourceBranch: checkout.SourceBranch,
Provider: provider,
WorkItemID: workItemID,
},
}, true
}
func buildAuthoringInstructions(task storage.Task, checkout workitempipeline.CheckoutMetadata) string {
var b strings.Builder
b.WriteString("You are a workspace agent running inside a NomadCode slot checkout.\n")
b.WriteString("Your task is to author or update the agent-roadmap Milestone file for the following Plane work item.\n\n")
b.WriteString("Use the roadmap skill to create or update the Milestone under the active Phase, then commit and push to the develop branch.\n\n")
b.WriteString("Work item title: " + task.Title + "\n")
b.WriteString("Workspace slot path: " + checkout.SlotPath + "\n")
b.WriteString("Source branch: " + checkout.SourceBranch + "\n")
return b.String()
}