update authoring request and model, scheduler tests, archive task files

This commit is contained in:
toki 2026-06-14 06:47:41 +09:00
parent 8803d29554
commit 91e3aabd16
7 changed files with 280 additions and 56 deletions

View file

@ -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-1] Preserve Full Work Item Identity For Authoring | [x] |
| [API-2] Idempotent Milestone Draft Instructions | [ ] | | [API-2] Idempotent Milestone Draft Instructions | [x] |
| [API-3] Scheduler Authoring Contract Regression | [ ] | | [API-3] Scheduler Authoring Contract Regression | [x] |
## 구현 체크리스트 ## 구현 체크리스트
- [ ] `09_creation_trigger_gate` predecessor complete.log가 active 또는 archive 경로에 있는지 확인하고, 없으면 구현하지 않고 review stub의 사용자 리뷰 요청이 아니라 plan dependency 미충족으로 보고한다. - [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에 포함된다. - [x] 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 금지 조건을 명시한다. - [x] fallback authoring instructions에 1상위 티켓=1마일스톤, 기존 Provider identity 검색 후 갱신, `## Provider identity` block 작성, `develop` commit/push, duplicate 금지 조건을 명시한다.
- [ ] focused Go tests와 `git diff --check`를 실행한다. - [x] focused Go tests와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. - [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트 ## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. > **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. > 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. - [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. - [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다. - [x] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다. - [x] 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하는지 확인한다. - [x] `.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` 파일을 남기지 않는다. - [x] 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 경로에서 이 체크리스트를 갱신한다. - [x] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. - [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. - [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`를 작성하지 않는다. - [ ] 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이면 `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로 이동한다. - [ ] 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 중간 검증 ### API-1 중간 검증
```bash ```bash
$ cd services/core && go test -count=1 ./internal/authoring ./internal/adapters/openai $ 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 중간 검증 ### API-2 중간 검증
```bash ```bash
$ cd services/core && go test -count=1 ./internal/authoring ./internal/roadmapsync $ 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 중간 검증 ### API-3 중간 검증
```bash ```bash
$ cd services/core && go test -count=1 ./internal/scheduler $ cd services/core && go test -count=1 ./internal/scheduler
(output) ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.011s
``` ```
### 최종 검증 ### 최종 검증
```bash ```bash
$ cd services/core && go test -count=1 ./internal/authoring ./internal/roadmapsync ./internal/adapters/openai ./internal/scheduler $ 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 $ git diff --check
(output) (출력 없음 - 정상 종료)
``` ```
--- ---
@ -156,3 +157,22 @@ Sections and their ownership:
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan | | 리뷰어를 위한 체크포인트 | 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 | | 검증 결과 (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 | | 코드리뷰 결과 | 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 기준도 확인했다.

View file

@ -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
- 없음
## 후속 작업
- 없음

View file

@ -46,29 +46,69 @@ func isAuthoringTask(task storage.Task) (workitempipeline.CheckoutMetadata, bool
return *meta.Checkout, true return *meta.Checkout, true
} }
// providerIdentity resolves the Plane provider and work item ID for a task. type workItemIdentity struct {
// Priority: storage.Task.ExternalProvider/ExternalID (set by workitem pipeline) Provider string
// → work_item payload map → task.Source / task.ID fallback. Tenant string
func providerIdentity(task storage.Task, payload pipelinePayload) (provider, workItemID string) { Project string
provider = task.Source WorkItemID string
workItemID = task.ID 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 != "" { if task.ExternalProvider != nil && *task.ExternalProvider != "" {
provider = *task.ExternalProvider ident.Provider = *task.ExternalProvider
} }
if task.ExternalID != nil && *task.ExternalID != "" { if task.ExternalID != nil && *task.ExternalID != "" {
workItemID = *task.ExternalID ident.WorkItemID = *task.ExternalID
return provider, workItemID }
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 != "" { 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 // BuildAuthoringGenerateInput constructs a workspace-bound model.GenerateInput
@ -97,11 +137,11 @@ func BuildAuthoringGenerateInput(task storage.Task) (model.GenerateInput, bool)
} }
instructions = strings.TrimSpace(p.Instructions) instructions = strings.TrimSpace(p.Instructions)
if instructions == "" { ident := getWorkItemIdentity(task, p)
instructions = buildAuthoringInstructions(task, checkout)
}
provider, workItemID := providerIdentity(task, p) if instructions == "" {
instructions = buildAuthoringInstructions(task, checkout, ident)
}
return model.GenerateInput{ return model.GenerateInput{
Input: input, Input: input,
@ -113,18 +153,46 @@ func BuildAuthoringGenerateInput(task storage.Task) (model.GenerateInput, bool)
WorkspaceMetadata: &model.WorkspaceMetadata{ WorkspaceMetadata: &model.WorkspaceMetadata{
Path: checkout.SlotPath, Path: checkout.SlotPath,
SourceBranch: checkout.SourceBranch, SourceBranch: checkout.SourceBranch,
Provider: provider, Provider: ident.Provider,
WorkItemID: workItemID, WorkItemID: ident.WorkItemID,
Tenant: ident.Tenant,
Project: ident.Project,
ExternalURL: ident.ExternalURL,
StateID: ident.StateID,
}, },
}, true }, 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 var b strings.Builder
b.WriteString("You are a workspace agent running inside a NomadCode slot checkout.\n") 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("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("Workspace slot path: " + checkout.SlotPath + "\n")
b.WriteString("Source branch: " + checkout.SourceBranch + "\n") b.WriteString("Source branch: " + checkout.SourceBranch + "\n")
return b.String() return b.String()

View file

@ -2,6 +2,7 @@ package authoring
import ( import (
"encoding/json" "encoding/json"
"strings"
"testing" "testing"
"github.com/nomadcode/nomadcode-core/internal/storage" "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") 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))
}

View file

@ -18,6 +18,10 @@ type WorkspaceMetadata struct {
SourceBranch string `json:"source_branch,omitempty"` SourceBranch string `json:"source_branch,omitempty"`
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
WorkItemID string `json:"work_item_id,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 { type GenerateInput struct {

View file

@ -505,6 +505,12 @@ func TestRunTaskRoutesPlaneOriginToAuthoringBridge(t *testing.T) {
if capturedInput.WorkspaceMetadata.SourceBranch != "develop" { if capturedInput.WorkspaceMetadata.SourceBranch != "develop" {
t.Errorf("unexpected source branch: %q", capturedInput.WorkspaceMetadata.SourceBranch) 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) { func TestRunTaskAuthoringPreemptsA2AClient(t *testing.T) {