diff --git a/bloom-runtime/src/agent_reconciliation.rs b/bloom-runtime/src/agent_reconciliation.rs index c84cba6b..8aac305b 100644 --- a/bloom-runtime/src/agent_reconciliation.rs +++ b/bloom-runtime/src/agent_reconciliation.rs @@ -12,7 +12,19 @@ const WRITER_ROLES: &[&str] = &[ "design-system", "designer", "frontend", + "frontend-ui", + "frontend-state", "backend", + "backend-api", + "backend-domain", + "integration", + "test-automation", + "performance", + "observability", + "database", + "security", + "devops", + "accessibility", "data-marketing", "documentation", "debug-router", diff --git a/bloom-runtime/src/agent_runtime.rs b/bloom-runtime/src/agent_runtime.rs index 4daafccc..98f7b4b3 100644 --- a/bloom-runtime/src/agent_runtime.rs +++ b/bloom-runtime/src/agent_runtime.rs @@ -69,7 +69,19 @@ const ALLOWED_ROLES: &[&str] = &[ "design-system", "designer", "frontend", + "frontend-ui", + "frontend-state", "backend", + "backend-api", + "backend-domain", + "integration", + "test-automation", + "performance", + "observability", + "database", + "security", + "devops", + "accessibility", "data-marketing", "code-review", "reviewer", @@ -256,7 +268,19 @@ fn is_repository_writer(role: &str) -> bool { "design-system" | "designer" | "frontend" + | "frontend-ui" + | "frontend-state" | "backend" + | "backend-api" + | "backend-domain" + | "integration" + | "test-automation" + | "performance" + | "observability" + | "database" + | "security" + | "devops" + | "accessibility" | "data-marketing" | "documentation" | "debug-router" @@ -410,13 +434,13 @@ fn agent_prompt(input: &AgentTaskRuntimeInput, branch: Option<&str>) -> String { branch.unwrap_or("unknown") ) } else if is_review_role(&input.role) { - "You are an independent verification/review worker. Do not modify product source files or create a feature branch. Inspect the actual repository and dependency PRs directly. Run the checks appropriate to your role. When reviewing a PR, leave a concise top-level PR comment prefixed with your Luna Agent ID and an evidence-based verdict. Do not pretend GitHub native self-approval is an independent approval when all agents share one GitHub credential.".to_string() + "You are an independent verification/review worker. Do not modify product source files or create a feature branch. Inspect the actual repository and dependency PRs directly. Run the checks appropriate to your role. When reviewing a PR, leave a concise top-level PR comment prefixed with your Bloom Agent ID and an evidence-based verdict. Do not pretend GitHub native self-approval is an independent approval when all agents share one GitHub credential.".to_string() } else { "You are an independent analysis worker. Inspect available repository and dependency evidence, produce a concrete task result, and do not modify product source files unless the task contract explicitly requires repository changes.".to_string() }; format!( - "You are Luna Agent `{agent_id}` ({team_name} / {role}).\n\n{mode}\n\nTask: {task_id} — {title}\n{summary}\n\nAcceptance criteria:\n{criteria}\n\nOriginal Product Owner request:\n{user_request}\n\nProduct summary:\n{product_summary}\n\nArchitecture summary:\n{architecture_summary}\n\nDependency evidence:\n{dependencies}\n\nRules:\n- Inspect real repository evidence before material decisions.\n- Do not blindly trust PM, Reviewer, Code Review, QA, or another Agent; independently verify relevant claims.\n- Every material action must have a defensible reason based on requirements, repository state, tests, runtime evidence, or explicit Product Owner direction.\n- Do not invent test results, metrics, user research, credentials, deployments, or external-service state.\n- If verification cannot be run, record the exact blocker instead of calling it passed.\n- Never expose secrets in logs, commits, PRs, reports, or documentation.\n- Return only the structured JSON report required by Luna.\n", + "You are Bloom Agent `{agent_id}` ({team_name} / {role}).\n\n{mode}\n\nTask: {task_id} — {title}\n{summary}\n\nAcceptance criteria:\n{criteria}\n\nOriginal Product Owner request:\n{user_request}\n\nProduct summary:\n{product_summary}\n\nArchitecture summary:\n{architecture_summary}\n\nDependency evidence:\n{dependencies}\n\nRules:\n- Inspect real repository evidence before material decisions.\n- Do not blindly trust PM, Reviewer, Code Review, QA, or another Agent; independently verify relevant claims.\n- Every material action must have a defensible reason based on requirements, repository state, tests, runtime evidence, or explicit Product Owner direction.\n- Do not invent test results, metrics, user research, credentials, deployments, or external-service state.\n- If verification cannot be run, record the exact blocker instead of calling it passed.\n- Never expose secrets in logs, commits, PRs, reports, or documentation.\n- Return only the structured JSON report required by Bloom.\n", agent_id = input.agent_id, team_name = input.team_name, role = input.role, @@ -546,8 +570,8 @@ fn run_app_server_agent( "id": 0, "params": { "clientInfo": { - "name": "luna_project_teams", - "title": "Luna Project Teams", + "name": "bloom_project_teams", + "title": "Bloom Project Teams", "version": "0.1.0" }, "capabilities": {} @@ -566,7 +590,7 @@ fn run_app_server_agent( "cwd": worktree.to_string_lossy(), "approvalPolicy": "never", "sandbox": "workspace-write", - "serviceName": "luna_project_teams" + "serviceName": "bloom_project_teams" } }), )?; @@ -832,4 +856,4 @@ pub async fn dispatch_agent_task(input: AgentTaskRuntimeInput) -> Result String { format!( - r#"You are the independent PM Codex Agent for Luna team {team_name} ({team_id}). + r#"You are the independent PM Codex Agent for Bloom team {team_name} ({team_id}). Project ID: {project_id} GitHub Organization: {organization} User request: {request} -Your job in this turn is planning only. Do not create files, repositories, branches, commits, PRs, or deployments. Return the project plan that Luna will execute after this turn. +Your job in this turn is planning only. Do not create files, repositories, branches, commits, PRs, or deployments. Return the project plan that Bloom will execute after this turn. Operating contract: - Treat the project as a real production service, not a demo or mock-only prototype. @@ -666,7 +690,13 @@ Operating contract: - Branch convention is agent///. taskSlug must be concise lowercase ASCII kebab-case. - Agents do not blindly trust PM or reviewers. Every material action must have a defensible, verifiable reason. - Code Review, higher-level Reviewer, QA, Documentation, User A, User B, and Process Evaluator should be included as independent gates for a normal user-facing production service. Omit a role only when it genuinely does not apply. -- Frontend and Backend tasks may run in parallel when dependencies allow it. +- Split large implementation work across independent roles when ownership boundaries are clear instead of assigning one giant Frontend or Backend task. +- Use frontend-ui for pages/components/layout/style/interactions, frontend-state for forms/state/data-fetching/cache/client data flow, and frontend for app shell or final frontend integration when useful. +- Use backend-api for endpoints/contracts/validation/controllers, backend-domain for services/use-cases/business rules, and backend for server shell or final backend integration when useful. +- Use integration for external APIs/webhooks/realtime/adapters, test-automation for automated unit/integration/E2E harness work, performance for measured bottleneck work, and observability for logs/health/metrics/tracing/error diagnosis. +- Database, Security, DevOps, and Accessibility are specialized implementation roles and should receive their own tasks when the project actually needs those concerns. +- Prefer tasks with minimal shared-file ownership so independent branches can progress safely and later run in parallel. Do not invent parallelism when two tasks inherently need the same implementation boundary. +- Frontend-family and Backend-family tasks may run independently when dependencies allow it. - Acceptance criteria must be observable and verifiable. Include build/test/browser/error/loading/empty/security requirements where relevant. - Do not assume external credentials, paid services, or unavailable datasets exist. Model them as explicit implementation blockers or setup tasks when necessary. - Keep tasks independently reviewable. Avoid one giant frontend or backend task covering the whole project. @@ -725,7 +755,7 @@ fn run_pm_codex( return Err("Codex CLI 로그인이 필요합니다. `codex login`을 실행해 주세요.".to_string()); } if !preflight.codex_chatgpt_auth { - return Err("Luna는 ChatGPT 로그인 상태의 Codex만 실행합니다.".to_string()); + return Err("Bloom은 ChatGPT 로그인 상태의 Codex만 실행합니다.".to_string()); } let planning_dir = PathBuf::from(workspace_root) diff --git a/bloom-runtime/src/replan_runtime.rs b/bloom-runtime/src/replan_runtime.rs index 7c2ff4cd..1669c134 100644 --- a/bloom-runtime/src/replan_runtime.rs +++ b/bloom-runtime/src/replan_runtime.rs @@ -14,7 +14,19 @@ const ALLOWED_TASK_ROLES: &[&str] = &[ "design-system", "designer", "frontend", + "frontend-ui", + "frontend-state", "backend", + "backend-api", + "backend-domain", + "integration", + "test-automation", + "performance", + "observability", + "database", + "security", + "devops", + "accessibility", "code-review", "reviewer", "qa", @@ -27,7 +39,20 @@ const REPOSITORY_WRITER_ROLES: &[&str] = &[ "design-system", "designer", "frontend", + "frontend-ui", + "frontend-state", "backend", + "backend-api", + "backend-domain", + "integration", + "test-automation", + "performance", + "observability", + "database", + "security", + "devops", + "accessibility", + "data-marketing", "documentation", "debug-router", ]; @@ -81,7 +106,19 @@ const REPLAN_SCHEMA: &str = r#"{ "design-system", "designer", "frontend", + "frontend-ui", + "frontend-state", "backend", + "backend-api", + "backend-domain", + "integration", + "test-automation", + "performance", + "observability", + "database", + "security", + "devops", + "accessibility", "code-review", "reviewer", "qa", @@ -510,7 +547,7 @@ fn prompt(input: &ReplanProjectInput) -> Result { let reopenable = input.reopenable_task_ids.join(", "); Ok(format!( - r#"You are the independent PM Codex Agent for Luna team {team_name} ({team_id}). + r#"You are the independent PM Codex Agent for Bloom team {team_name} ({team_id}). Project: {project_id} Existing repository: {repository} @@ -531,24 +568,26 @@ Failure route evidence: Current PM tasks and actual runtime status: {tasks} -Tasks Luna permits you to retire because they have no completed/external Git work: +Tasks Bloom permits you to retire because they have no completed/external Git work: {retirable} -Tasks Luna permits you to reopen on their existing branch/worktree where applicable: +Tasks Bloom permits you to reopen on their existing branch/worktree where applicable: {reopenable} Replan attempt: {replan_attempt}/{max_attempts} -Your job is a repair replan only. You MUST NOT create a repository, rename the repository, modify files, commit, push, merge, deploy, or claim tests passed. Luna will apply only the validated plan operations you return. +Your job is a repair replan only. You MUST NOT create a repository, rename the repository, modify files, commit, push, merge, deploy, or claim tests passed. Bloom will apply only the validated plan operations you return. Rules: - Preserve completed/external Git work. You cannot edit existing Task definitions in this response. - `retireTaskIds` may contain only IDs from the retirable list. - `reopenTaskIds` may contain only IDs from the reopenable list. Use reopen when the existing Agent/branch should own the fix or its verification must be repeated. - `newTasks` are appended repair Tasks. Their IDs must be brand-new and must not reuse any current Task ID, even a retired one. +- New repair Tasks may use the implementation swarm roles: frontend-ui, frontend-state, backend-api, backend-domain, integration, test-automation, performance, observability, plus database, security, devops, and accessibility when those are the real repair owners. +- Split a repair into a specialist role only when the ownership boundary is concrete; do not create extra Agents merely to appear parallel. - Do not create a normal `debug-router` Task; the dedicated Debug Router already ran. - New dependencies may reference current non-retired Tasks or other new Tasks only. -- Resolve the escalated failed Task: either retire it, reopen it, or reopen one of its actual upstream Tasks so that Luna can rewind and re-run the failed downstream chain. +- Resolve the escalated failed Task: either retire it, reopen it, or reopen one of its actual upstream Tasks so that Bloom can rewind and re-run the failed downstream chain. - Every repository-changing Task in the resulting DAG must still have a downstream Code Review → Reviewer → QA chain. - Prefer the smallest repair plan that addresses the evidence. Do not rewrite unrelated product scope. - If the evidence actually requires a Product Owner choice or unavailable credential instead of PM replanning, do not invent a workaround. Return a minimal safe plan only if one truly exists; otherwise the Runtime validation may reject this proposal and keep the project blocked. diff --git a/bloom-runtime/ts/catalog.policy-test.ts b/bloom-runtime/ts/catalog.policy-test.ts new file mode 100644 index 00000000..ad227d22 --- /dev/null +++ b/bloom-runtime/ts/catalog.policy-test.ts @@ -0,0 +1,44 @@ +import { createInitialProjectTeamsState } from "./catalog"; +import { SPECIALIST_AGENT_ROLES } from "./specialistPlanning"; +import type { ExecutableAgentRole } from "./types"; + +const IMPLEMENTATION_ACCELERATOR_ROLES: ExecutableAgentRole[] = [ + "frontend-ui", + "frontend-state", + "backend-api", + "backend-domain", + "integration", + "test-automation", + "performance", + "observability", +]; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +function testEveryTeamHasExpandedRoster() { + const state = createInitialProjectTeamsState(); + const expandedRoles = [...IMPLEMENTATION_ACCELERATOR_ROLES, ...SPECIALIST_AGENT_ROLES]; + + assert(state.teams.length === 5, "Bloom must keep the five peer delivery teams"); + for (const team of state.teams) { + assert(team.agents.length === 27, `${team.id} must contain 27 Agents after implementation expansion`); + assert(new Set(team.agents.map((agent) => agent.id)).size === 27, `${team.id} Agent IDs must remain unique`); + + for (const role of expandedRoles) { + const agent = team.agents.find((candidate) => candidate.role === role); + assert(agent, `${team.id} must include a ${role} Agent`); + assert(agent.id === `${team.id}:${role}`, `${team.id} ${role} Agent must use the stable team:role identity`); + assert(agent.autonomy === "independent", `${team.id} ${role} Agent must remain independently accountable`); + assert(agent.minimumExperienceYears === 10, `${team.id} ${role} Agent must use the senior operating standard`); + } + } +} + +function run() { + testEveryTeamHasExpandedRoster(); + console.log("catalog policy tests passed"); +} + +run(); diff --git a/bloom-runtime/ts/catalog.ts b/bloom-runtime/ts/catalog.ts index 194f658c..c9793221 100644 --- a/bloom-runtime/ts/catalog.ts +++ b/bloom-runtime/ts/catalog.ts @@ -16,7 +16,7 @@ export const BOUQUET_AUTH_POLICY = { export const EXECUTION_POLICY = { id: "iseol-workflow" as const, - version: "1.4.0", + version: "1.6.0", summary: "모든 Agent는 이설 작업 방식처럼 실제 저장소를 기준으로 독립적으로 작업하며 10년 이상 시니어 수준의 품질 기준을 적용합니다.", rules: [ "모든 Agent는 자신의 전문 역할에서 최소 10년 이상 실무를 수행한 시니어 수준의 판단, 품질 기준, 리스크 감각을 적용합니다.", @@ -27,6 +27,8 @@ export const EXECUTION_POLICY = { "검증하지 못한 항목은 성공했다고 기록하지 않고 정확한 blocker를 남깁니다.", "커밋은 작은 작업 단위로 나누고 영어 커밋 메시지를 사용합니다.", "작업한 Agent가 직접 branch push와 PR 생성/업데이트를 수행합니다.", + "PM은 큰 Frontend/Backend 일을 UI, 상태·데이터, API, 도메인, 외부 연동, 테스트 자동화, 성능, 관측 가능성처럼 독립 가능한 구현 경계로 분리해 여러 Agent가 병렬로 처리할 수 있게 계획합니다.", + "Database, Security, DevOps, Accessibility 전문 Agent는 관련 위험이나 구현 범위가 있을 때 별도 Task와 PR로 독립 검증합니다.", "Code Review Agent가 코드 품질을 독립 검토하고 Reviewer Agent가 기능/요구사항/구조를 별도로 검토합니다.", "QA Agent는 실제 build/test/사용 흐름을 검증하고 Documentation Agent는 검증된 사실을 기준으로 사용자·개발·운영·마케팅 문서를 맞춥니다.", "Data & Marketing Agent는 제품, 사용자, 채널, 퍼널, 지표, SEO/콘텐츠, 출시 실험을 근거 중심으로 분석하고 Documentation Agent가 검증·정리할 수 있는 마케팅 산출물을 남깁니다.", @@ -41,7 +43,8 @@ export const WORKFLOW_STAGES = [ "Planning", "Design System", "Designer", - "Frontend / Backend", + "Frontend / Backend Implementation Swarm", + "Database / Security / DevOps / Accessibility", "Code Review", "Reviewer", "QA", @@ -62,8 +65,20 @@ const agentCatalog: Array<{ { role: "pm", label: "PM Agent", description: "요구사항, Task, Agent 실행 순서를 관리" }, { role: "design-system", label: "Design System Agent", description: "Figma와 실제 제품 근거로 디자인 규칙을 결정" }, { role: "designer", label: "Designer Agent", description: "제품 화면과 상호작용을 설계" }, - { role: "frontend", label: "Frontend Agent", description: "실제 프론트엔드 저장소를 구현" }, - { role: "backend", label: "Backend Agent", description: "API, DB, 서버 영역을 구현" }, + { role: "frontend", label: "Frontend Agent", description: "프론트엔드 전체 구조와 최종 통합을 구현" }, + { role: "frontend-ui", label: "Frontend UI Agent", description: "페이지, 컴포넌트, 레이아웃, 스타일과 사용자 인터랙션 구현" }, + { role: "frontend-state", label: "Frontend State & Data Agent", description: "상태 관리, 폼, 데이터 패칭, 캐시, 클라이언트 데이터 흐름 구현" }, + { role: "backend", label: "Backend Agent", description: "백엔드 전체 구조와 서버 통합을 구현" }, + { role: "backend-api", label: "API Agent", description: "HTTP/RPC endpoint, contract, validation, controller와 transport 계층 구현" }, + { role: "backend-domain", label: "Domain Logic Agent", description: "서비스, 유스케이스, 비즈니스 규칙과 도메인 로직 구현" }, + { role: "integration", label: "Integration Agent", description: "외부 API, webhook, realtime, third-party adapter와 내부 서비스 연결 구현" }, + { role: "test-automation", label: "Test Automation Agent", description: "unit, integration, E2E 자동화 테스트와 fixture/harness 구현" }, + { role: "performance", label: "Performance Agent", description: "렌더링, API, 쿼리, 캐시, 번들 및 병목 구간을 측정하고 개선" }, + { role: "observability", label: "Observability Agent", description: "로그, health check, metric, tracing, 오류 진단과 운영 가시성 구현" }, + { role: "database", label: "Database Agent", description: "스키마, 마이그레이션, 쿼리, 트랜잭션, 데이터 무결성과 저장 성능을 구현·검증" }, + { role: "security", label: "Security Agent", description: "인증·인가, 입력 검증, secret, 의존성, 민감정보와 공격 표면을 독립 검증" }, + { role: "devops", label: "DevOps Agent", description: "CI/CD, 실행 환경, 배포 설정, 컨테이너, 관측 가능성과 재현 가능한 운영 경로를 구현·검증" }, + { role: "accessibility", label: "Accessibility Agent", description: "시맨틱 UI, 키보드·포커스, 스크린리더, 폼 오류와 접근성 사용 흐름을 구현·검증" }, { role: "data-marketing", label: "Data & Marketing Agent", description: "제품 데이터, 사용자 세그먼트, 퍼널, 채널, SEO/콘텐츠, 출시 실험을 분석하고 마케팅 전략 문서를 작성" }, { role: "code-review", label: "Code Review Agent", description: "PR diff의 코드 품질, 버그, 보안, 성능, 테스트 누락을 독립 검토" }, { role: "reviewer", label: "Reviewer Agent", description: "기능, 요구사항, 구조와 제품 완성도를 독립 검토" }, diff --git a/bloom-runtime/ts/orchestrationCore.ts b/bloom-runtime/ts/orchestrationCore.ts index d487bb4e..53adf962 100644 --- a/bloom-runtime/ts/orchestrationCore.ts +++ b/bloom-runtime/ts/orchestrationCore.ts @@ -1,6 +1,7 @@ import { ensureBouquetAuthPlan } from "./bouquetAuth"; import { ensureMarketingDocumentationPlan } from "./dataMarketing"; import { validateProjectPlanReviewTopology } from "./planTopology"; +import { ensureSpecialistAgentPlan } from "./specialistPlanning"; import type { ExecutableAgentRole, ProjectPlan, @@ -23,7 +24,8 @@ export type TaskRunSummary = { export function prepareOrchestrationPlan(plan: ProjectPlan): ProjectPlan { const authPlan = ensureBouquetAuthPlan(plan); - const prepared = ensureMarketingDocumentationPlan(authPlan); + const specialistPlan = ensureSpecialistAgentPlan(authPlan); + const prepared = ensureMarketingDocumentationPlan(specialistPlan); validateProjectPlanReviewTopology(prepared); return prepared; } @@ -119,7 +121,7 @@ export function projectStatusForActiveRoles( { roles: ["user-a", "user-b"], status: "user-test" }, { roles: ["qa"], status: "qa" }, { roles: ["code-review", "reviewer", "documentation", "data-marketing"], status: "review" }, - { roles: ["frontend", "backend", "debug-router"], status: "development" }, + { roles: ["frontend", "frontend-ui", "frontend-state", "backend", "backend-api", "backend-domain", "integration", "test-automation", "performance", "observability", "database", "security", "devops", "accessibility", "debug-router"], status: "development" }, { roles: ["design-system", "designer"], status: "design" }, { roles: ["idea"], status: "planning" }, ]; diff --git a/bloom-runtime/ts/planTopology.ts b/bloom-runtime/ts/planTopology.ts index 15f5b10b..a3ea320d 100644 --- a/bloom-runtime/ts/planTopology.ts +++ b/bloom-runtime/ts/planTopology.ts @@ -5,7 +5,19 @@ export const REPOSITORY_WRITER_ROLES: Array> = [ "design-system", "designer", "frontend", + "frontend-ui", + "frontend-state", "backend", + "backend-api", + "backend-domain", + "integration", + "test-automation", + "performance", + "observability", + "database", + "security", + "devops", + "accessibility", "data-marketing", "documentation", "debug-router", diff --git a/bloom-runtime/ts/specialistPlanning.policy-test.ts b/bloom-runtime/ts/specialistPlanning.policy-test.ts new file mode 100644 index 00000000..8a69481c --- /dev/null +++ b/bloom-runtime/ts/specialistPlanning.policy-test.ts @@ -0,0 +1,129 @@ +import { ensureSpecialistAgentPlan, SPECIALIST_AGENT_ROLES } from "./specialistPlanning"; +import type { ProjectPlan, ProjectTaskPlan } from "./types"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +function fullStackPlan(): ProjectPlan { + return { + projectName: "Specialist Fixture", + repositoryName: "specialist-fixture", + productSummary: "A user-facing full-stack web application with authentication and SQLite persistence.", + architectureSummary: "React frontend, API backend, SQLite database, CI build and production deployment path.", + needsAuth: true, + technologyDecisions: [ + { area: "database", choice: "SQLite", reason: "small durable relational store" }, + { area: "delivery", choice: "CI", reason: "reproducible build and test gate" }, + ], + tasks: [ + { + id: "FE-001", + title: "Frontend", + role: "frontend", + taskSlug: "frontend-app", + summary: "Implement the accessible user interface.", + dependsOn: [], + acceptanceCriteria: ["frontend build passes"], + }, + { + id: "BE-001", + title: "Backend", + role: "backend", + taskSlug: "backend-api", + summary: "Implement authenticated API and persistence.", + dependsOn: [], + acceptanceCriteria: ["API tests pass"], + }, + ], + }; +} + +function testFullStackSpecialists() { + const prepared = ensureSpecialistAgentPlan(fullStackPlan()); + const roles = new Set(prepared.tasks.map((task) => task.role)); + + for (const role of SPECIALIST_AGENT_ROLES) { + assert(roles.has(role), `full-stack plan must activate ${role} Agent`); + } + + const database = prepared.tasks.find((task) => task.role === "database"); + const security = prepared.tasks.find((task) => task.role === "security"); + const accessibility = prepared.tasks.find((task) => task.role === "accessibility"); + const devops = prepared.tasks.find((task) => task.role === "devops"); + + assert(database, "Database Agent task must exist"); + assert(security, "Security Agent task must exist"); + assert(accessibility, "Accessibility Agent task must exist"); + assert(devops, "DevOps Agent task must exist"); + assert(database.dependsOn.includes("BE-001"), "Database Agent must wait for backend implementation evidence"); + assert(security.dependsOn.includes(database.id), "Security Agent must inspect the database specialist result when present"); + assert(accessibility.dependsOn.includes("FE-001"), "Accessibility Agent must inspect frontend implementation evidence"); + assert(devops.dependsOn.includes(security.id), "DevOps Agent must consume security hardening evidence when present"); +} + +function testSpecialistRoutingIsIdempotent() { + const once = ensureSpecialistAgentPlan(fullStackPlan()); + const twice = ensureSpecialistAgentPlan(once); + + for (const role of SPECIALIST_AGENT_ROLES) { + assert( + twice.tasks.filter((task) => task.role === role).length === 1, + `${role} specialist routing must not duplicate an existing specialist task`, + ); + } +} + +function testSmallNonImplementationPlanStaysSmall() { + const plan: ProjectPlan = { + projectName: "Research Note", + repositoryName: "research-note", + productSummary: "Clarify a product idea without implementation.", + architectureSummary: "No application implementation is in scope.", + needsAuth: false, + technologyDecisions: [], + tasks: [ + { + id: "IDEA-001", + title: "Clarify scope", + role: "idea", + taskSlug: "clarify-scope", + summary: "Clarify the product scope.", + dependsOn: [], + acceptanceCriteria: ["scope is documented"], + }, + ], + }; + + const prepared = ensureSpecialistAgentPlan(plan); + assert(prepared.tasks.length === 1, "specialists must not be forced into a non-implementation plan without relevant signals"); +} + +function testMarketingTaskBudgetIsProtected() { + const tasks: ProjectTaskPlan[] = Array.from({ length: 35 }, (_, index) => ({ + id: `IDEA-${String(index + 1).padStart(3, "0")}`, + title: `Task ${index + 1}`, + role: "idea", + taskSlug: `task-${index + 1}`, + summary: index === 0 ? "database security deployment accessibility frontend backend" : "planning task", + dependsOn: [], + acceptanceCriteria: ["complete"], + })); + const plan: ProjectPlan = { + ...fullStackPlan(), + tasks, + }; + + const prepared = ensureSpecialistAgentPlan(plan); + assert(prepared.tasks.length === 35, "specialist routing must preserve the mandatory marketing task budget"); +} + +function run() { + testFullStackSpecialists(); + testSpecialistRoutingIsIdempotent(); + testSmallNonImplementationPlanStaysSmall(); + testMarketingTaskBudgetIsProtected(); + console.log("specialistPlanning policy tests passed"); +} + +run(); diff --git a/bloom-runtime/ts/specialistPlanning.ts b/bloom-runtime/ts/specialistPlanning.ts new file mode 100644 index 00000000..a6d186a5 --- /dev/null +++ b/bloom-runtime/ts/specialistPlanning.ts @@ -0,0 +1,186 @@ +import type { ExecutableAgentRole, ProjectPlan, ProjectTaskPlan } from "./types"; + +export const SPECIALIST_AGENT_ROLES = [ + "database", + "security", + "devops", + "accessibility", +] as const satisfies readonly ExecutableAgentRole[]; + +const MAX_TASKS_BEFORE_MANDATORY_MARKETING = 35; + +type SpecialistRole = (typeof SPECIALIST_AGENT_ROLES)[number]; + +type SpecialistDefinition = { + role: SpecialistRole; + prefix: string; + slug: string; + title: string; + summary: string; + acceptanceCriteria: string[]; + shouldActivate: (plan: ProjectPlan, text: string) => boolean; + dependencyRoles: ExecutableAgentRole[]; +}; + +function normalizedPlanText(plan: ProjectPlan) { + return [ + plan.productSummary, + plan.architectureSummary, + ...plan.technologyDecisions.flatMap((decision) => [decision.area, decision.choice, decision.reason]), + ...plan.tasks.flatMap((task) => [task.title, task.summary, ...task.acceptanceCriteria]), + ].join(" ").toLowerCase(); +} + +function includesAny(text: string, values: string[]) { + return values.some((value) => text.includes(value)); +} + +function hasAnyRole(plan: ProjectPlan, roles: ExecutableAgentRole[]) { + return plan.tasks.some((task) => roles.includes(task.role)); +} + +function nextTaskId(tasks: ProjectTaskPlan[], prefix: string) { + const used = new Set(tasks.map((task) => task.id)); + for (let index = 1; index <= 999; index += 1) { + const candidate = `${prefix}-${String(index).padStart(3, "0")}`; + if (!used.has(candidate)) return candidate; + } + throw new Error(`${prefix} specialist Task ID를 생성할 수 없습니다.`); +} + +function nextTaskSlug(tasks: ProjectTaskPlan[], base: string) { + const used = new Set(tasks.map((task) => task.taskSlug)); + if (!used.has(base)) return base; + for (let index = 2; index <= 99; index += 1) { + const candidate = `${base}-${index}`; + if (!used.has(candidate)) return candidate; + } + throw new Error(`${base} specialist taskSlug를 생성할 수 없습니다.`); +} + +function terminalTaskIds(tasks: ProjectTaskPlan[]) { + const referenced = new Set(tasks.flatMap((task) => task.dependsOn)); + return tasks.filter((task) => !referenced.has(task.id)).map((task) => task.id); +} + +function dependenciesForRoles(tasks: ProjectTaskPlan[], roles: ExecutableAgentRole[]) { + const roleTaskIds = tasks.filter((task) => roles.includes(task.role)).map((task) => task.id); + return roleTaskIds.length > 0 ? roleTaskIds : terminalTaskIds(tasks); +} + +const frontendImplementationRoles: ExecutableAgentRole[] = [ + "frontend", + "frontend-ui", + "frontend-state", +]; +const backendImplementationRoles: ExecutableAgentRole[] = [ + "backend", + "backend-api", + "backend-domain", + "integration", +]; +const allImplementationRoles: ExecutableAgentRole[] = [ + ...frontendImplementationRoles, + ...backendImplementationRoles, + "test-automation", + "performance", + "observability", +]; + +const definitions: SpecialistDefinition[] = [ + { + role: "database", + prefix: "DB", + slug: "database-integrity", + title: "데이터 모델 및 저장 계층 검증", + summary: "Database Agent가 실제 구현을 기준으로 스키마, 마이그레이션, 쿼리, 트랜잭션, 인덱스와 데이터 무결성을 검토하고 필요한 저장 계층 변경을 별도 branch/PR로 구현합니다. 근거 없는 성능 수치는 만들지 않고 재현 가능한 테스트와 migration evidence를 남깁니다.", + acceptanceCriteria: [ + "실제 도메인 요구사항과 데이터 모델, 제약조건, 관계가 일치한다.", + "스키마 변경이 있다면 재현 가능한 migration 또는 동등한 버전 관리 경로가 존재한다.", + "핵심 읽기/쓰기 경로의 트랜잭션 경계, 무결성, 오류 처리가 검증된다.", + "쿼리 또는 저장 계층 변경에 가능한 자동 테스트나 실행 evidence가 남는다.", + ], + shouldActivate: (plan, text) => hasAnyRole(plan, backendImplementationRoles) + || includesAny(text, ["database", " db ", "sql", "postgres", "mysql", "sqlite", "schema", "migration", "persistence", "storage"]), + dependencyRoles: backendImplementationRoles, + }, + { + role: "security", + prefix: "SEC", + slug: "security-hardening", + title: "보안 경계 및 위협 표면 검증", + summary: "Security Agent가 인증·인가, 입력 검증, 민감정보, secret, 의존성, 권한 경계와 주요 공격 표면을 실제 코드와 설정에서 독립 검증하고 필요한 보안 수정을 별도 branch/PR로 구현합니다. 다른 Agent의 보안 주장을 그대로 신뢰하지 않고 evidence를 직접 확인합니다.", + acceptanceCriteria: [ + "인증·인가가 필요한 경로의 권한 경계와 실패 동작이 실제 코드에서 검증된다.", + "외부 입력과 민감 데이터 처리 경로에서 검증·노출·보존 위험을 확인한다.", + "secret 값이 repository, 로그, 사용자 응답에 하드코딩되지 않는다.", + "발견한 위험과 수정 사항에 재현 가능한 테스트 또는 명확한 검증 evidence를 남긴다.", + ], + shouldActivate: (plan, text) => plan.needsAuth + || includesAny(text, ["auth", "login", "permission", "security", "secret", "token", "payment", "admin", "sensitive"]), + dependencyRoles: [...frontendImplementationRoles, ...backendImplementationRoles, "database"], + }, + { + role: "accessibility", + prefix: "A11Y", + slug: "accessibility-validation", + title: "접근성 사용자 흐름 검증", + summary: "Accessibility Agent가 실제 사용자 화면을 기준으로 시맨틱 구조, 키보드·포커스, 스크린리더 이름, 폼/오류 전달과 주요 접근성 흐름을 독립 검증하고 필요한 수정을 별도 branch/PR로 구현합니다. 시각적 추정만으로 통과시키지 않고 가능한 자동·수동 evidence를 남깁니다.", + acceptanceCriteria: [ + "핵심 사용자 흐름이 키보드만으로 진행 가능하고 포커스 이동이 예측 가능하다.", + "상호작용 요소에 적절한 시맨틱 요소와 접근 가능한 이름이 제공된다.", + "폼 상태, 오류, 로딩 등 중요한 상태가 시각 정보에만 의존하지 않는다.", + "가능한 접근성 검사 또는 재현 가능한 수동 검증 결과가 evidence로 남는다.", + ], + shouldActivate: (plan, text) => hasAnyRole(plan, ["design-system", "designer", ...frontendImplementationRoles]) + || includesAny(text, ["accessibility", "accessible", "a11y", "screen reader", "keyboard", "responsive ui"]), + dependencyRoles: ["design-system", "designer", ...frontendImplementationRoles], + }, + { + role: "devops", + prefix: "OPS", + slug: "delivery-runtime-readiness", + title: "빌드·배포·운영 재현성 검증", + summary: "DevOps Agent가 CI/CD, 환경 변수 계약, 실행·배포 설정, 컨테이너 또는 호스팅 경로, 로그와 기본 관측 가능성을 실제 repository 기준으로 검증하고 필요한 운영 변경을 별도 branch/PR로 구현합니다. 실제로 검증하지 않은 배포 성공이나 인프라 존재를 주장하지 않습니다.", + acceptanceCriteria: [ + "깨끗한 환경에서 의존성 설치, build/test, 실행에 필요한 명령과 설정이 재현 가능하다.", + "환경 변수와 secret 경계가 문서/설정에 명확하며 실제 secret 값은 저장소에 포함되지 않는다.", + "CI 또는 동등한 자동 검증 경로가 현재 프로젝트의 핵심 build/test를 실행한다.", + "배포 또는 운영 경로에서 확인하지 못한 항목은 성공으로 표시하지 않고 blocker/evidence를 남긴다.", + ], + shouldActivate: (plan, text) => hasAnyRole(plan, allImplementationRoles) + || includesAny(text, ["deploy", "deployment", "ci/cd", "docker", "container", "infrastructure", "production", "runtime", "hosting"]), + dependencyRoles: [...allImplementationRoles, "database", "security"], + }, +]; + +export function ensureSpecialistAgentPlan(plan: ProjectPlan): ProjectPlan { + const existingSpecialists = new Set( + plan.tasks + .filter((task) => SPECIALIST_AGENT_ROLES.includes(task.role as SpecialistRole)) + .map((task) => task.role as SpecialistRole), + ); + const text = normalizedPlanText(plan); + const tasks = [...plan.tasks]; + let remainingSlots = Math.max(0, MAX_TASKS_BEFORE_MANDATORY_MARKETING - tasks.length); + + for (const definition of definitions) { + if (remainingSlots === 0) break; + if (existingSpecialists.has(definition.role) || !definition.shouldActivate(plan, text)) continue; + + const task: ProjectTaskPlan = { + id: nextTaskId(tasks, definition.prefix), + title: definition.title, + role: definition.role, + taskSlug: nextTaskSlug(tasks, definition.slug), + summary: definition.summary, + dependsOn: dependenciesForRoles(tasks, definition.dependencyRoles), + acceptanceCriteria: definition.acceptanceCriteria, + }; + tasks.push(task); + existingSpecialists.add(definition.role); + remainingSlots -= 1; + } + + return tasks.length === plan.tasks.length ? plan : { ...plan, tasks }; +} diff --git a/bloom-runtime/ts/types.ts b/bloom-runtime/ts/types.ts index f3619c31..91227af7 100644 --- a/bloom-runtime/ts/types.ts +++ b/bloom-runtime/ts/types.ts @@ -8,7 +8,19 @@ export type AgentRole = | "design-system" | "designer" | "frontend" + | "frontend-ui" + | "frontend-state" | "backend" + | "backend-api" + | "backend-domain" + | "integration" + | "test-automation" + | "performance" + | "observability" + | "database" + | "security" + | "devops" + | "accessibility" | "data-marketing" | "code-review" | "reviewer" diff --git a/bloom-runtime/tsconfig.policy-tests.json b/bloom-runtime/tsconfig.policy-tests.json index b0b1192a..71c23fc1 100644 --- a/bloom-runtime/tsconfig.policy-tests.json +++ b/bloom-runtime/tsconfig.policy-tests.json @@ -10,6 +10,10 @@ }, "include": [ "ts/types.ts", + "ts/policies.ts", + "ts/permissions.ts", + "ts/catalog.ts", + "ts/catalog.policy-test.ts", "ts/teamAllocation.ts", "ts/teamAllocation.policy-test.ts", "ts/intakeClarification.ts", @@ -18,6 +22,8 @@ "ts/dataMarketing.policy-test.ts", "ts/seniorAgent.ts", "ts/seniorAgent.policy-test.ts", + "ts/specialistPlanning.ts", + "ts/specialistPlanning.policy-test.ts", "ts/planTopology.ts", "ts/e2eSmoke.ts", "ts/e2eSmoke.policy-test.ts",