diff --git a/agent-task/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/CODE_REVIEW-local-G06.md b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/code_review_local_G06_0.log similarity index 70% rename from agent-task/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/CODE_REVIEW-local-G06.md rename to agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/code_review_local_G06_0.log index d616863..3cf9bde 100644 --- a/agent-task/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/CODE_REVIEW-local-G06.md +++ b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/code_review_local_G06_0.log @@ -42,43 +42,46 @@ task=m-milestone-work-item-creation-sync/10+09_plane_draft_authoring, plan=0, ta | 항목 | 완료 여부 | |------|---------| -| [API-1] Preserve Full Work Item Identity For Authoring | [ ] | -| [API-2] Idempotent Milestone Draft Instructions | [ ] | -| [API-3] Scheduler Authoring Contract Regression | [ ] | +| [API-1] Preserve Full Work Item Identity For Authoring | [x] | +| [API-2] Idempotent Milestone Draft Instructions | [x] | +| [API-3] Scheduler Authoring Contract Regression | [x] | ## 구현 체크리스트 -- [ ] `09_creation_trigger_gate` predecessor complete.log가 active 또는 archive 경로에 있는지 확인하고, 없으면 구현하지 않고 review stub의 사용자 리뷰 요청이 아니라 plan dependency 미충족으로 보고한다. -- [ ] Plane work item identity context를 authoring input에서 구조적으로 보존한다. 검증: provider, tenant, project, work item id가 instructions 또는 client-side metadata에 포함된다. -- [ ] fallback authoring instructions에 1상위 티켓=1마일스톤, 기존 Provider identity 검색 후 갱신, `## Provider identity` block 작성, `develop` commit/push, duplicate 금지 조건을 명시한다. -- [ ] focused Go tests와 `git diff --check`를 실행한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. +- [x] `09_creation_trigger_gate` predecessor complete.log가 active 또는 archive 경로에 있는지 확인하고, 없으면 구현하지 않고 review stub의 사용자 리뷰 요청이 아니라 plan dependency 미충족으로 보고한다. +- [x] Plane work item identity context를 authoring input에서 구조적으로 보존한다. 검증: provider, tenant, project, work item id가 instructions 또는 client-side metadata에 포함된다. +- [x] fallback authoring instructions에 1상위 티켓=1마일스톤, 기존 Provider identity 검색 후 갱신, `## Provider identity` block 작성, `develop` commit/push, duplicate 금지 조건을 명시한다. +- [x] focused Go tests와 `git diff --check`를 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. ## 코드리뷰 전용 체크리스트 > **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. > 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. -- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다. -- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다. -- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. -- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. -- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. -- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. -- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. - [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. - [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. - [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. ## 계획 대비 변경 사항 -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ +계획과 동일하게 정상 구현되었으며, 변경 사항은 없습니다. ## 주요 설계 결정 -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ +- `workItemIdentity` 구조체와 `getWorkItemIdentity` 헬퍼 함수를 추가하여 payload `work_item` 및 task external fields(ExternalProvider, ExternalID, ExternalUrl, ExternalMetadata) 모두에서 tenant, project, external_url, state_id 정보를 구조적으로 파싱하도록 하였습니다. +- `model.WorkspaceMetadata` 구조체에 `Tenant`, `Project`, `ExternalURL`, `StateID` 필드들을 추가하여 full identity context를 보존하도록 하였습니다. +- OpenAI 어댑터의 `buildRequestMetadata`에서 `metadata.workspace` API payload는 기존과 동일하게 flat `workspace` path 만을 가지도록 유지하여 호환성을 확보하였습니다. +- `buildAuthoringInstructions`에서 instructions 본문에 규칙(1상위 티켓=1마일스톤, 기존 Provider identity 검색 후 갱신, `## Provider identity` block 작성, `develop` commit/push, duplicate 금지 조건 등)을 명확하게 명시하고, 추출된 work item 상세 속성들도 본문에 동적으로 추가하여 모델이 올바르게 Milestone을 작성하도록 유도했습니다. ## 사용자 리뷰 요청 @@ -101,39 +104,37 @@ _기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외 ## 검증 결과 -_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ - -필수 규칙: -- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. -- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. -- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. -- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. -- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. +중간 및 최종 검증 명령 실행 결과는 다음과 같습니다. ### API-1 중간 검증 ```bash $ cd services/core && go test -count=1 ./internal/authoring ./internal/adapters/openai -(output) +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.003s +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.009s ``` ### API-2 중간 검증 ```bash $ cd services/core && go test -count=1 ./internal/authoring ./internal/roadmapsync -(output) +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.003s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.003s ``` ### API-3 중간 검증 ```bash $ cd services/core && go test -count=1 ./internal/scheduler -(output) +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.011s ``` ### 최종 검증 ```bash $ cd services/core && go test -count=1 ./internal/authoring ./internal/roadmapsync ./internal/adapters/openai ./internal/scheduler -(output) +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.003s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.003s +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.006s +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.011s $ git diff --check -(output) +(출력 없음 - 정상 종료) ``` --- @@ -156,3 +157,22 @@ Sections and their ownership: | 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan | | 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry | | 코드리뷰 결과 | Review agent appends | Not included in stub | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS - `complete.log` 작성 후 active task directory를 archive로 이동한다. + +리뷰 메모: +- `09_creation_trigger_gate` 선행 `complete.log`는 `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/09_creation_trigger_gate/complete.log`에서 확인했다. +- 리뷰 중 `services/core/internal/authoring/request_test.go`의 contains helper 임시 주석/수동 구현은 비동작성 Nit로 직접 정리했다. +- 추가 검증으로 `cd services/core && go test -count=1 ./...`를 실행해 core 전체 smoke 기준도 확인했다. diff --git a/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/complete.log b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/complete.log new file mode 100644 index 0000000..2250358 --- /dev/null +++ b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/complete.log @@ -0,0 +1,44 @@ +# Complete - m-milestone-work-item-creation-sync/10+09_plane_draft_authoring + +## 완료 일시 + +2026-06-14 + +## 요약 + +Plane-origin Milestone draft authoring contract를 보강했고, 1회 리뷰 루프에서 PASS로 종결했다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | PASS | Provider identity context 보존, idempotent Milestone draft instructions, scheduler authoring regression 검증 완료 | + +## 구현/정리 내용 + +- `authoring` 입력에 provider, tenant, project, work item id, external URL, state id를 보존하는 `workItemIdentity` 경로를 추가했다. +- fallback authoring instructions에 Provider identity block 작성/검색, 기존 Milestone 갱신 우선, duplicate 금지, develop push 계약을 명시했다. +- OpenAI adapter의 `metadata.workspace` flat string wire shape를 유지한 채 client-side `WorkspaceMetadata`만 additive 확장했다. +- scheduler authoring route가 계속 model bridge로 향하는지 regression assertion을 보강했다. +- 리뷰 중 `services/core/internal/authoring/request_test.go`의 contains helper 임시 주석/수동 구현을 비동작성 Nit로 정리했다. + +## 최종 검증 + +- `cd services/core && go test -count=1 ./internal/authoring ./internal/roadmapsync ./internal/adapters/openai ./internal/scheduler` - PASS; focused packages all returned `ok`. +- `cd services/core && go test -count=1 ./...` - PASS; core 전체 package가 `ok` 또는 `[no test files]`로 종료했다. +- `git diff --check` - PASS; 출력 없음. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md` +- Completed task ids: + - `plane-draft`: PASS; evidence=`agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/plan_local_G06_0.log`, `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/code_review_local_G06_0.log`; verification=`cd services/core && go test -count=1 ./internal/authoring ./internal/roadmapsync ./internal/adapters/openai ./internal/scheduler`, `cd services/core && go test -count=1 ./...`, `git diff --check` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/PLAN-local-G06.md b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/plan_local_G06_0.log similarity index 100% rename from agent-task/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/PLAN-local-G06.md rename to agent-task/archive/2026/06/m-milestone-work-item-creation-sync/10+09_plane_draft_authoring/plan_local_G06_0.log diff --git a/services/core/internal/authoring/request.go b/services/core/internal/authoring/request.go index 17b1fdf..83733e2 100644 --- a/services/core/internal/authoring/request.go +++ b/services/core/internal/authoring/request.go @@ -46,29 +46,69 @@ func isAuthoringTask(task storage.Task) (workitempipeline.CheckoutMetadata, bool 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 +type workItemIdentity struct { + Provider string + Tenant string + Project string + WorkItemID string + ExternalURL string + StateID string +} + +func getWorkItemIdentity(task storage.Task, payload pipelinePayload) workItemIdentity { + var ident workItemIdentity + + ident.Provider = task.Source + ident.WorkItemID = task.ID if task.ExternalProvider != nil && *task.ExternalProvider != "" { - provider = *task.ExternalProvider + ident.Provider = *task.ExternalProvider } if task.ExternalID != nil && *task.ExternalID != "" { - workItemID = *task.ExternalID - return provider, workItemID + ident.WorkItemID = *task.ExternalID + } + if task.ExternalUrl != nil && *task.ExternalUrl != "" { + ident.ExternalURL = *task.ExternalUrl } - // 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 + ident.Provider = p } - return provider, workItemID + if id := strings.TrimSpace(payload.WorkItem["id"]); id != "" { + ident.WorkItemID = id + } + if t := strings.TrimSpace(payload.WorkItem["tenant"]); t != "" { + ident.Tenant = t + } + if prj := strings.TrimSpace(payload.WorkItem["project"]); prj != "" { + ident.Project = prj + } + if urlStr := strings.TrimSpace(payload.WorkItem["external_url"]); urlStr != "" { + ident.ExternalURL = urlStr + } + if state := strings.TrimSpace(payload.WorkItem["state_id"]); state != "" { + ident.StateID = state + } + + if len(task.ExternalMetadata) > 0 && string(task.ExternalMetadata) != "null" && string(task.ExternalMetadata) != "{}" { + var extMeta map[string]any + if err := json.Unmarshal(task.ExternalMetadata, &extMeta); err == nil { + if t, ok := extMeta["tenant"].(string); ok && t != "" && ident.Tenant == "" { + ident.Tenant = t + } + if p, ok := extMeta["project"].(string); ok && p != "" && ident.Project == "" { + ident.Project = p + } + if s, ok := extMeta["state_id"].(string); ok && s != "" && ident.StateID == "" { + ident.StateID = s + } + if u, ok := extMeta["external_url"].(string); ok && u != "" && ident.ExternalURL == "" { + ident.ExternalURL = u + } + } + } + + return ident } // BuildAuthoringGenerateInput constructs a workspace-bound model.GenerateInput @@ -97,11 +137,11 @@ func BuildAuthoringGenerateInput(task storage.Task) (model.GenerateInput, bool) } instructions = strings.TrimSpace(p.Instructions) - if instructions == "" { - instructions = buildAuthoringInstructions(task, checkout) - } + ident := getWorkItemIdentity(task, p) - provider, workItemID := providerIdentity(task, p) + if instructions == "" { + instructions = buildAuthoringInstructions(task, checkout, ident) + } return model.GenerateInput{ Input: input, @@ -113,18 +153,46 @@ func BuildAuthoringGenerateInput(task storage.Task) (model.GenerateInput, bool) WorkspaceMetadata: &model.WorkspaceMetadata{ Path: checkout.SlotPath, SourceBranch: checkout.SourceBranch, - Provider: provider, - WorkItemID: workItemID, + Provider: ident.Provider, + WorkItemID: ident.WorkItemID, + Tenant: ident.Tenant, + Project: ident.Project, + ExternalURL: ident.ExternalURL, + StateID: ident.StateID, }, }, true } -func buildAuthoringInstructions(task storage.Task, checkout workitempipeline.CheckoutMetadata) string { +func buildAuthoringInstructions(task storage.Task, checkout workitempipeline.CheckoutMetadata, ident workItemIdentity) 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("Please strictly follow these rules:\n") + b.WriteString("- One Plane top-level work item maps to exactly one agent-roadmap Milestone.\n") + b.WriteString("- First search active Milestones for a Provider identity block matching provider/tenant/project/work item id; update that file if found.\n") + b.WriteString("- If none exists, create one Milestone under the active Phase and include a `## Provider identity` block.\n") + b.WriteString("- The block must include provider, tenant, project, work item id, and milestone id/roadmap item id when known.\n") + b.WriteString("- Commit and push the Milestone file change to develop from the slot checkout.\n") + b.WriteString("- Do not create duplicate Milestones or Plane child work items in this slice.\n\n") + + b.WriteString("Work item details:\n") + b.WriteString("Title: " + task.Title + "\n") + b.WriteString("Provider: " + ident.Provider + "\n") + if ident.Tenant != "" { + b.WriteString("Tenant: " + ident.Tenant + "\n") + } + if ident.Project != "" { + b.WriteString("Project: " + ident.Project + "\n") + } + b.WriteString("Work Item ID: " + ident.WorkItemID + "\n") + if ident.ExternalURL != "" { + b.WriteString("External URL: " + ident.ExternalURL + "\n") + } + if ident.StateID != "" { + b.WriteString("State ID: " + ident.StateID + "\n") + } + b.WriteString("\nCheckout details:\n") b.WriteString("Workspace slot path: " + checkout.SlotPath + "\n") b.WriteString("Source branch: " + checkout.SourceBranch + "\n") return b.String() diff --git a/services/core/internal/authoring/request_test.go b/services/core/internal/authoring/request_test.go index c9cb45b..356be8b 100644 --- a/services/core/internal/authoring/request_test.go +++ b/services/core/internal/authoring/request_test.go @@ -2,6 +2,7 @@ package authoring import ( "encoding/json" + "strings" "testing" "github.com/nomadcode/nomadcode-core/internal/storage" @@ -183,3 +184,84 @@ func TestBuildAuthoringGenerateInputEmptySlotPath(t *testing.T) { t.Error("expected task with empty slot path to not be detected as authoring task") } } + +func TestBuildAuthoringGenerateInputCarriesProviderIdentityContext(t *testing.T) { + pipelineBody := `{"message":"Plane ticket body text","work_item":{"provider":"plane","tenant":"acme","project":"proj-1","id":"NOMAD-42","state_id":"backlog","external_url":"https://plane.example.com/NOMAD-42"}}` + extID := "NOMAD-42" + extProvider := "plane" + extURL := "https://plane.example.com/NOMAD-42" + extMeta := json.RawMessage(`{"tenant":"acme-fallback","project":"proj-fallback","state_id":"state-fallback","external_url":"https://fallback.com"}`) + + task := storage.Task{ + ID: "task-2", + Title: "fallback title", + Source: "plane", + Payload: json.RawMessage(pipelineBody), + Metadata: checkoutMeta("/home/user/workspace/nomadcode/slots/001", "develop"), + ExternalProvider: &extProvider, + ExternalID: &extID, + ExternalUrl: &extURL, + ExternalMetadata: extMeta, + } + + input, ok := BuildAuthoringGenerateInput(task) + if !ok { + t.Fatal("expected authoring task to be detected") + } + + if input.WorkspaceMetadata == nil { + t.Fatal("expected WorkspaceMetadata to be set") + } + + // Payload values should take precedence or fallback to task fields + if input.WorkspaceMetadata.Provider != "plane" { + t.Errorf("unexpected provider: %q", input.WorkspaceMetadata.Provider) + } + if input.WorkspaceMetadata.WorkItemID != "NOMAD-42" { + t.Errorf("unexpected work_item_id: %q", input.WorkspaceMetadata.WorkItemID) + } + if input.WorkspaceMetadata.Tenant != "acme" { // payload value + t.Errorf("unexpected tenant: %q", input.WorkspaceMetadata.Tenant) + } + if input.WorkspaceMetadata.Project != "proj-1" { // payload value + t.Errorf("unexpected project: %q", input.WorkspaceMetadata.Project) + } + if input.WorkspaceMetadata.ExternalURL != "https://plane.example.com/NOMAD-42" { // task/payload value + t.Errorf("unexpected external URL: %q", input.WorkspaceMetadata.ExternalURL) + } + if input.WorkspaceMetadata.StateID != "backlog" { // payload value + t.Errorf("unexpected state id: %q", input.WorkspaceMetadata.StateID) + } +} + +func TestBuildAuthoringGenerateInputFallbackInstructionsRequireIdentityBlockAndNoDuplicate(t *testing.T) { + task := storage.Task{ + ID: "task-2", + Title: "My work item title", + Source: "plane", + Payload: json.RawMessage(`{}`), + Metadata: checkoutMeta("/home/user/workspace/nomadcode/slots/001", "develop"), + } + + input, ok := BuildAuthoringGenerateInput(task) + if !ok { + t.Fatal("expected authoring task to be detected") + } + + assertions := []string{ + "Provider identity block", + "duplicate", + "active Milestone", + "develop", + } + + for _, ast := range assertions { + if !containsIgnoreCase(input.Instructions, ast) { + t.Errorf("expected instructions to contain %q, but got:\n%s", ast, input.Instructions) + } + } +} + +func containsIgnoreCase(s, substr string) bool { + return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) +} diff --git a/services/core/internal/model/model.go b/services/core/internal/model/model.go index 4406cb4..b059728 100644 --- a/services/core/internal/model/model.go +++ b/services/core/internal/model/model.go @@ -18,6 +18,10 @@ type WorkspaceMetadata struct { SourceBranch string `json:"source_branch,omitempty"` Provider string `json:"provider,omitempty"` WorkItemID string `json:"work_item_id,omitempty"` + Tenant string `json:"tenant,omitempty"` + Project string `json:"project,omitempty"` + ExternalURL string `json:"external_url,omitempty"` + StateID string `json:"state_id,omitempty"` } type GenerateInput struct { diff --git a/services/core/internal/scheduler/jobs_test.go b/services/core/internal/scheduler/jobs_test.go index c8e4e54..9626e4f 100644 --- a/services/core/internal/scheduler/jobs_test.go +++ b/services/core/internal/scheduler/jobs_test.go @@ -505,6 +505,12 @@ func TestRunTaskRoutesPlaneOriginToAuthoringBridge(t *testing.T) { if capturedInput.WorkspaceMetadata.SourceBranch != "develop" { t.Errorf("unexpected source branch: %q", capturedInput.WorkspaceMetadata.SourceBranch) } + if capturedInput.WorkspaceMetadata.Provider != "plane" { + t.Errorf("unexpected provider: %q", capturedInput.WorkspaceMetadata.Provider) + } + if capturedInput.WorkspaceMetadata.WorkItemID != "task-plane-1" { + t.Errorf("unexpected work_item_id: %q", capturedInput.WorkspaceMetadata.WorkItemID) + } } func TestRunTaskAuthoringPreemptsA2AClient(t *testing.T) {