diff --git a/client/src/pages/SeatsPage.tsx b/client/src/pages/SeatsPage.tsx index 0fe6dda..f473aaf 100644 --- a/client/src/pages/SeatsPage.tsx +++ b/client/src/pages/SeatsPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { KeyRound, Plus, Bot, Wand2 } from 'lucide-react' +import { KeyRound, Plus, Bot, Sparkles, Wand2 } from 'lucide-react' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' import { Badge } from '@/components/ui/badge' @@ -337,6 +337,14 @@ export function SeatsPage() {
+ {selected && ( + setAgent({ ...agent, skillKeys: keys })} + /> + )}
{skills.map((skill) => (
) } + +/** Maps a free-text seat role name to skill role tags — any role can be AI-staffed. */ +function roleTagsFor(roleName: string): string[] { + const n = roleName.toLowerCase() + const tags: string[] = [] + if (n.includes('product') || n.includes('owner') || n.includes('pm')) tags.push('product-owner') + if (n.includes('qa') || n.includes('test') || n.includes('quality')) tags.push('qa') + if (n.includes('engineer') || n.includes('dev') || n.includes('programmer') || n.includes('backend') || n.includes('frontend')) tags.push('engineer') + if (n.includes('design') || n.includes('ux') || n.includes('ui')) tags.push('designer') + if (n.includes('analyst') || n.includes('analysis') || n.includes('business')) tags.push('analyst') + return tags +} + +/** Suggests the skill set matching the seat's role — one click staffs any role with AI. */ +function SuggestedSkills({ + roleName, + skills, + current, + onApply, +}: { + roleName: string + skills: { skillKey: string; name: string; roles: string[] }[] + current: string[] + onApply: (keys: string[]) => void +}) { + const tags = roleTagsFor(roleName) + const suggested = skills.filter((s) => s.roles.some((r) => tags.includes(r))) + if (suggested.length === 0) return null + + const keys = suggested.map((s) => s.skillKey) + const applied = keys.every((k) => current.includes(k)) + + return ( +
+ + + Suggested for “{roleName}”: {suggested.map((s) => s.name).join(', ')} + + +
+ ) +} diff --git a/skills/bug-diagnosis/SKILL.md b/skills/bug-diagnosis/SKILL.md new file mode 100644 index 0000000..c30b121 --- /dev/null +++ b/skills/bug-diagnosis/SKILL.md @@ -0,0 +1,37 @@ +--- +id: bug-diagnosis +name: Bug Diagnosis +version: 1.0.0 +summary: From a bug report and code context, find the root cause and propose the fix. +roles: [engineer] +inputs: A bug report (symptoms, repro steps) and any relevant code or logs attached to the task. +outputs: Root-cause analysis, the proposed fix as a patch sketch, and a regression test suggestion. +actions: + - name: diagnose-bug + risk: draft + description: Post the diagnosis + proposed fix as a draft artifact on the task (held for review). +tools: [] +context: [house-style, repo-docs] +visibility: public +min_tier: free +golden_tests: + - input: | + Bug: after logout, pressing Back shows the dashboard with stale user data. + Context: the dashboard reads from a client-side cache keyed by user id. + expected: | + Root cause: the client cache is not cleared on logout, so navigation restores stale + state. Fix: clear the cache in logout(); regression test: logout then navigate back + asserts a redirect to /login and an empty cache. +--- + +# Bug Diagnosis + +You are a software engineer on call. Work the bug like a scientist: + +1. **Reproduce in your head** — restate the failure path from the symptoms. +2. **Root cause** — the deepest cause the evidence supports, not the first plausible one. + Quote the specific code/log lines that implicate it. +3. **Proposed fix** — a minimal patch sketch at the root cause, not a symptom bandage. +4. **Regression test** — what test would have caught this. + +If the evidence is insufficient, list exactly what extra context you need. Never guess silently. diff --git a/skills/code-implementation/SKILL.md b/skills/code-implementation/SKILL.md new file mode 100644 index 0000000..9186569 --- /dev/null +++ b/skills/code-implementation/SKILL.md @@ -0,0 +1,41 @@ +--- +id: code-implementation +name: Code Implementation +version: 1.0.0 +summary: Implement a story as a reviewable patch — code with reasoning, ready for human review. +roles: [engineer] +inputs: A story with acceptance criteria, plus any relevant code context attached to the task. +outputs: A unified-diff style patch (or complete new files) with a short implementation note. +actions: + - name: implement-code + risk: draft + description: Produce the patch as a draft artifact on the task (held for review). Direct Git write-back is Phase 2. +tools: [] +context: [house-style, repo-docs] +visibility: public +min_tier: free +golden_tests: + - input: | + Story: clicking logout must clear the session and redirect to /login. + Context: React app; auth lives in useAuth() with a logout() action. + expected: | + Patch: header component — add a Logout button calling useAuth().logout() then + navigate('/login'); note: guard the button behind isAuthenticated. +--- + +# Code Implementation + +You are a software engineer. Implement exactly what the story's acceptance criteria require. + +Rules: + +- Output a **patch**: unified-diff hunks for edited files, or full content for new files, + each preceded by its path. +- Follow the codebase's existing conventions visible in the provided context. No drive-by + refactors — stay inside the story's scope. +- After the patch, add an **implementation note**: what changed, why, and anything the + reviewer should look at closely (edge cases, trade-offs). +- If an acceptance criterion cannot be met with the available context, say so explicitly + instead of inventing APIs. + +Your output is reviewed by a human before anything lands — write for that reviewer. diff --git a/skills/requirements-analysis/SKILL.md b/skills/requirements-analysis/SKILL.md new file mode 100644 index 0000000..4b89b55 --- /dev/null +++ b/skills/requirements-analysis/SKILL.md @@ -0,0 +1,38 @@ +--- +id: requirements-analysis +name: Requirements Analysis +version: 1.0.0 +summary: Turn raw stakeholder notes into structured, testable requirements. +roles: [analyst, product-owner] +inputs: Raw notes — meeting minutes, customer feedback, a feature wish, or a vague request. +outputs: Structured requirements — goals, user stories with acceptance criteria, assumptions, and open questions. +actions: + - name: analyze-requirements + risk: draft + description: Produce the requirements document as a draft artifact on the task (held for review). +tools: [] +context: [house-style, product-docs] +visibility: public +min_tier: free +golden_tests: + - input: "Customer call: they keep losing work, want some kind of autosave, maybe every minute or so?" + expected: | + Goal: no user loses more than one minute of work. + Story: as an editor, my changes save automatically so a crash loses at most 60s. + Acceptance: edits persist within 60s without manual save; recovery prompt on reopen. + Open question: conflict behaviour when two sessions edit the same document. +--- + +# Requirements Analysis + +You are a business analyst. Extract what the stakeholder actually needs from what they said. + +Produce, in order: + +- **Goal** — the outcome in one sentence, measurable where possible. +- **User stories** — "as a …, I … so that …", each with verifiable acceptance criteria. +- **Assumptions** — what you inferred that a stakeholder should confirm. +- **Open questions** — ambiguities that block implementation, phrased so a yes/no or short + answer resolves them. + +Do not invent scope. Anything not grounded in the input belongs under assumptions or questions. diff --git a/skills/ui-design-spec/SKILL.md b/skills/ui-design-spec/SKILL.md new file mode 100644 index 0000000..13a0dd2 --- /dev/null +++ b/skills/ui-design-spec/SKILL.md @@ -0,0 +1,38 @@ +--- +id: ui-design-spec +name: UI Design Spec +version: 1.0.0 +summary: Turn a feature into a concrete screen spec — layout, components, states, and flows. +roles: [designer] +inputs: A feature or story, plus the product's design language notes if attached. +outputs: A screen-by-screen spec — layout, components, interaction states, and the user flow. +actions: + - name: write-design-spec + risk: draft + description: Produce the design spec as a draft artifact on the task (held for review). +tools: [] +context: [house-style, design-system] +visibility: public +min_tier: free +golden_tests: + - input: "Feature: users need a way to log out from anywhere in the app." + expected: | + Placement: avatar menu, top-right header, last item "Log out" with icon. + States: confirm none (instant), loading spinner on click, redirect to /login. + Flow: any page → avatar menu → Log out → /login with a "signed out" toast. +--- + +# UI Design Spec + +You are a product designer. Specify the screen(s) so a developer can build them without +guessing. + +For each screen or surface: + +- **Layout** — regions and hierarchy (what's where, and why). +- **Components** — name them in the product's design system terms where possible. +- **States** — empty, loading, error, success, and permission-restricted variants. +- **Flow** — entry points, the happy path, and exits. +- **Copy** — exact labels for buttons, titles, and empty states. + +Stay inside the existing design language; flag any new pattern you introduce and justify it. diff --git a/tests/TeamUp.IntegrationTests/AnyRoleSeatTests.cs b/tests/TeamUp.IntegrationTests/AnyRoleSeatTests.cs new file mode 100644 index 0000000..380ece0 --- /dev/null +++ b/tests/TeamUp.IntegrationTests/AnyRoleSeatTests.cs @@ -0,0 +1,148 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using Microsoft.Extensions.DependencyInjection; +using TeamUp.Modules.Assembler.Queue; +using TeamUp.Modules.Assembler.Runtime; +using Xunit; + +namespace TeamUp.IntegrationTests; + +/// +/// The core product thesis: ANY seat can be AI-staffed — a role is just a name + skill atoms. +/// An "Engineer" seat (not PO, not QA) runs the same pipeline: skills assemble, the model is +/// called, and the implement-code proposal is held for review like any other governed action. +/// +public sealed class AnyRoleSeatTests(PostgresFixture postgres) : IClassFixture +{ + private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId); + + private sealed record IdResponse(Guid Id); + + private sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name); + + private sealed record SeatResponse(Guid Id, Guid TeamId, string RoleName, string State, Guid? MemberId, Guid? AgentId); + + private sealed record SyncResult(int Indexed); + + private sealed record RunResponse( + Guid Id, Guid SeatId, Guid WorkItemId, Guid? AgentId, string Status, + string? ActionType, string? ActionRisk, string? Prompt, string? Output, string? Error); + + private sealed record ReviewItemResponse( + Guid Id, Guid OrganizationId, Guid TeamId, Guid AgentRunId, Guid AgentId, Guid WorkItemId, + string ActionKind, string Risk, string Title, string Content, List ChildTitles, + string? Trace, string Status, string? Decision, double? EditDistance, DateTimeOffset CreatedAtUtc); + + [Fact] + public async Task An_engineer_seat_runs_the_same_governed_pipeline() + { + var settings = new Dictionary + { + ["GitSource:Provider"] = "filesystem", + ["GitSource:Root"] = LocateSkillsDirectory(), + }; + + await using var factory = new TeamUpWebFactory(postgres.ConnectionString, settings); + using var anon = factory.CreateClient(); + + var owner = await PostOk(anon, "/api/identity/bootstrap", new + { + organizationName = "AliaSaaS", + ownerEmail = "owner@alia.test", + ownerDisplayName = "Owner", + ownerPassword = "Passw0rd!", + }); + using var client = Authed(factory, owner.Token); + + await client.PostAsJsonAsync("/api/orgboard/organizations", new { organizationId = owner.OrganizationId, name = "AliaSaaS" }); + var team = await PostOk(client, "/api/orgboard/teams", new { organizationId = owner.OrganizationId, name = "IPNOPS" }); + var config = await PostOk(client, "/api/integrations/api-configs", new + { + organizationId = owner.OrganizationId, + name = "Vertex-Pro", + provider = "stub", + model = "gemini-pro", + apiKey = "sk-demo-key", + }); + + // The catalogue now carries atoms for engineer/designer/analyst roles too. + var sync = await PostOk(client, "/api/skills/sync", new { }); + Assert.True(sync.Indexed >= 8); + + // Staff an ENGINEER seat with AI — same configurator, different atoms. + var seat = await PostOk(client, "/api/orgboard/seats", new { teamId = team.Id, roleName = "Backend Engineer" }); + await client.PostAsJsonAsync($"/api/orgboard/seats/{seat.Id}/agent", new + { + name = "Edison", + monogram = "ED", + autonomy = "Gated", + apiConfigId = config.Id, + skillKeys = new[] { "code-implementation", "bug-diagnosis" }, + docs = Array.Empty(), + }); + + var task = await PostOk(client, "/api/orgboard/tasks", new + { + teamId = team.Id, + title = "Implement the logout endpoint", + description = "POST /logout clears the session.", + type = "Story", + }); + + var run = await PostOk(client, "/api/assembler/runs", new { seatId = seat.Id, workItemId = task.Id }); + await DrainOneJob(factory); + + var done = await client.GetFromJsonAsync($"/api/assembler/runs/{run.Id}"); + Assert.Equal("Completed", done!.Status); + Assert.Equal("implement-code", done.ActionType); // the engineer atom's primary action + Assert.Equal("Draft", done.ActionRisk); + Assert.Contains("Code Implementation", done.Prompt); // the skill body assembled in + + // Gated engineer output is governed exactly like PO/QA output: held for human review. + var pending = await client.GetFromJsonAsync>( + $"/api/governance/reviews?organizationId={owner.OrganizationId}"); + var held = Assert.Single(pending!); + Assert.Equal("implement-code", held.ActionKind); + Assert.Equal(task.Id, held.WorkItemId); + } + + private sealed record RunTask(Guid Id); + + private static async Task DrainOneJob(TeamUpWebFactory factory) + { + await using var scope = factory.Services.CreateAsyncScope(); + var queue = scope.ServiceProvider.GetRequiredService(); + var job = await queue.ClaimNextAsync("test-worker"); + Assert.NotNull(job); + await scope.ServiceProvider.GetRequiredService().ProcessAsync(job!); + } + + private static HttpClient Authed(TeamUpWebFactory factory, string token) + { + var client = factory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + return client; + } + + private static async Task PostOk(HttpClient client, string url, object body) + { + var response = await client.PostAsJsonAsync(url, body); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var value = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(value); + return value!; + } + + private static string LocateSkillsDirectory() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "TeamUp.slnx"))) + { + dir = dir.Parent; + } + + Assert.NotNull(dir); + return Path.Combine(dir!.FullName, "skills"); + } +} diff --git a/tests/TeamUp.IntegrationTests/SkillSyncTests.cs b/tests/TeamUp.IntegrationTests/SkillSyncTests.cs index fb15580..e7310cc 100644 --- a/tests/TeamUp.IntegrationTests/SkillSyncTests.cs +++ b/tests/TeamUp.IntegrationTests/SkillSyncTests.cs @@ -49,7 +49,7 @@ public sealed class SkillSyncTests(PostgresFixture postgres) : IClassFixture(); - Assert.Equal(4, result!.Indexed); + Assert.True(result!.Indexed >= 8, $"expected all atoms indexed, got {result.Indexed}"); var productOwner = await client.GetFromJsonAsync>("/api/skills/?role=product-owner"); Assert.Contains(productOwner!, s => s.SkillKey == "spec-writing");