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() }