fix(ui): roles
This commit is contained in:
parent
7678efedc8
commit
36025c6c67
|
|
@ -70,6 +70,13 @@ class ProvisionOptions:
|
|||
|
||||
_ROLE_SOUL_MAX_CHARS = 24_000
|
||||
_ROLE_SOUL_WORD_RE = re.compile(r"[a-z0-9]+")
|
||||
_ROLE_INSTRUCTION_START = "<!-- mission-control:role-instructions:start -->"
|
||||
_ROLE_INSTRUCTION_END = "<!-- mission-control:role-instructions:end -->"
|
||||
_ROLE_INSTRUCTION_RE = re.compile(
|
||||
rf"{re.escape(_ROLE_INSTRUCTION_START)}.*?{re.escape(_ROLE_INSTRUCTION_END)}",
|
||||
re.DOTALL,
|
||||
)
|
||||
_ROLE_INSTRUCTION_PATCH_FILES = frozenset({"IDENTITY.md", "SOUL.md"})
|
||||
|
||||
|
||||
def _is_missing_session_error(exc: OpenClawGatewayError) -> bool:
|
||||
|
|
@ -290,11 +297,115 @@ def _identity_context(agent: Agent) -> dict[str, str]:
|
|||
return {**identity_context, **extra_identity_context}
|
||||
|
||||
|
||||
def _role_instruction_context(agent: Agent) -> dict[str, str]:
|
||||
role = _identity_context(agent).get("identity_role", "")
|
||||
return {
|
||||
"local_role_instruction_markdown": _local_role_instruction_markdown(role),
|
||||
}
|
||||
|
||||
|
||||
def _role_slug(role: str) -> str:
|
||||
tokens = _ROLE_SOUL_WORD_RE.findall(role.strip().lower())
|
||||
return "-".join(tokens)
|
||||
|
||||
|
||||
_ROLE_INSTRUCTION_MARKDOWN_BY_SLUG: dict[str, str] = {
|
||||
"backend-code": """## Role Instructions - Backend Code
|
||||
|
||||
- Own server-side behavior: APIs, services, data models, migrations, background jobs, integrations, and backend tests.
|
||||
- Start by reading the relevant backend route, schema, model, service, and migration paths before editing.
|
||||
- Preserve API contracts unless the task explicitly asks for a contract change; when a contract changes, update callers and generated clients as needed.
|
||||
- Add or update focused backend tests for changed behavior, including permission, validation, and persistence edge cases.
|
||||
- Call out risks around data integrity, backwards compatibility, idempotency, concurrency, and operational rollout in task comments.""",
|
||||
"ui": """## Role Instructions - UI
|
||||
|
||||
- Own the user-facing experience: page structure, component behavior, responsive layout, accessibility, and interaction polish.
|
||||
- Follow the existing design system and component patterns before adding new UI abstractions.
|
||||
- Check empty, loading, error, disabled, long-text, and mobile states for every changed workflow.
|
||||
- Keep copy clear and action-oriented; avoid explanatory clutter inside the app.
|
||||
- Verify visual changes with the strongest local signal available, such as lint, tests, builds, or screenshots when a browser workflow is practical.""",
|
||||
"security-review": """## Role Instructions - Security Review
|
||||
|
||||
- Review for concrete security risks before implementation convenience: auth boundaries, authorization, secrets, injection, unsafe redirects, SSRF, XSS, CSRF, file access, and auditability.
|
||||
- Treat trust boundaries explicitly. Identify who can call the path, what data they can reach, and what external side effects can happen.
|
||||
- Prefer minimal, targeted fixes that reduce exploitability without changing unrelated behavior.
|
||||
- Include regression tests or clear verification evidence for the security boundary being protected.
|
||||
- Report residual risk plainly, including assumptions and any follow-up hardening that should be tracked separately.""",
|
||||
"code-review": """## Role Instructions - Code Review
|
||||
|
||||
- Review before rewriting. Prioritize correctness, regressions, missing tests, maintainability, and operational risk.
|
||||
- Lead with specific findings tied to files, lines, payloads, or workflows; keep summaries secondary.
|
||||
- Distinguish must-fix defects from style preferences, and avoid broad refactors unless they directly reduce task risk.
|
||||
- Verify claims against the codebase and tests when possible.
|
||||
- If asked to implement review fixes, keep edits scoped to the identified issues and preserve unrelated user work.""",
|
||||
"qa": """## Role Instructions - QA
|
||||
|
||||
- Own verification strategy: expected behavior, edge cases, regression paths, and evidence quality.
|
||||
- Convert requirements into concrete test cases before executing or recommending changes.
|
||||
- Exercise happy paths, boundary cases, permissions, failure states, and cross-browser or responsive concerns when relevant.
|
||||
- Record reproducible steps for failures and include the smallest useful evidence in task comments.
|
||||
- Prefer automated regression coverage for behavior that is likely to recur.""",
|
||||
"devops": """## Role Instructions - DevOps
|
||||
|
||||
- Own deployment, runtime configuration, observability, infrastructure, CI/CD, and operational reliability concerns.
|
||||
- Check environment variables, secrets handling, logs, health checks, migrations, rollbacks, and resource limits.
|
||||
- Prefer idempotent, reversible operational changes with clear verification commands.
|
||||
- Avoid destructive infrastructure or production-impacting actions without explicit approval.
|
||||
- Document rollout risks, monitoring signals, and rollback paths when task changes affect operations.""",
|
||||
"data": """## Role Instructions - Data
|
||||
|
||||
- Own data shape, quality, querying, transformations, analytics, and persistence semantics.
|
||||
- Validate source assumptions, null behavior, units, time zones, deduplication, and migration/backfill needs.
|
||||
- Prefer structured parsers and typed schemas over ad hoc string handling.
|
||||
- Add focused checks for data integrity and representative edge cases.
|
||||
- Explain any data caveats, sampling limits, or confidence constraints in task comments.""",
|
||||
"documentation": """## Role Instructions - Documentation
|
||||
|
||||
- Own clarity, accuracy, structure, and maintenance burden of written artifacts.
|
||||
- Verify commands, paths, API names, and workflow steps against the repository before documenting them.
|
||||
- Write for the target reader's next action, not for exhaustive narration.
|
||||
- Keep docs consistent with existing terminology and formatting.
|
||||
- Flag stale or conflicting documentation discovered while working.""",
|
||||
"product": """## Role Instructions - Product
|
||||
|
||||
- Own user outcome, scope clarity, acceptance criteria, and tradeoff framing.
|
||||
- Translate ambiguity into concrete workflow, success metric, and done-signal questions or proposals.
|
||||
- Prioritize the smallest useful increment that preserves the intended user experience.
|
||||
- Consider permissions, onboarding, empty states, and operational support as part of the product surface.
|
||||
- Capture decisions and open questions in task comments so implementation stays aligned.""",
|
||||
"triage": """## Role Instructions - Triage
|
||||
|
||||
- Own intake clarity, prioritization, routing, and next-action definition.
|
||||
- Reproduce or validate the issue when possible, then classify severity, owner, affected surface, and likely cause.
|
||||
- Split vague work into actionable tasks with clear acceptance criteria and dependencies.
|
||||
- Route work to the best-fit agent role and explain why when the assignment is not obvious.
|
||||
- Keep comments concise: current state, blocker if any, next action, and owner.""",
|
||||
"generalist": """## Role Instructions - Generalist
|
||||
|
||||
- Adapt to the task context while staying inside the repository's existing architecture and conventions.
|
||||
- Read enough surrounding code to avoid local fixes that break adjacent workflows.
|
||||
- Prefer focused changes with direct verification evidence.
|
||||
- Escalate when the task needs a specialist lens such as security, data, UI, or backend architecture.
|
||||
- Leave task comments that make the outcome and remaining risk easy for the lead to understand.""",
|
||||
}
|
||||
|
||||
|
||||
def _local_role_instruction_markdown(role: str) -> str:
|
||||
role = role.strip()
|
||||
if not role:
|
||||
return ""
|
||||
markdown = _ROLE_INSTRUCTION_MARKDOWN_BY_SLUG.get(_role_slug(role))
|
||||
if markdown:
|
||||
return markdown
|
||||
return f"""## Role Instructions - {role}
|
||||
|
||||
- Treat `{role}` as your execution lens for assigned tasks.
|
||||
- Start each task by identifying the artifact you own, the quality bar for that role, and the evidence needed to prove completion.
|
||||
- Stay within the role unless the task or lead explicitly asks you to broaden scope.
|
||||
- When work crosses into another specialty, call that out and suggest the best-fit collaborator or review path.
|
||||
- Capture role-specific assumptions, risks, and verification evidence in task comments."""
|
||||
|
||||
|
||||
def _select_role_soul_ref(
|
||||
refs: list[souls_directory.SoulRef],
|
||||
*,
|
||||
|
|
@ -373,6 +484,7 @@ def _build_context(
|
|||
base_url = settings.base_url
|
||||
main_session_key = GatewayAgentIdentity.session_key(gateway)
|
||||
identity_context = _identity_context(agent)
|
||||
role_instruction_context = _role_instruction_context(agent)
|
||||
user_context = _user_context(user)
|
||||
return {
|
||||
"agent_name": agent.name,
|
||||
|
|
@ -402,6 +514,7 @@ def _build_context(
|
|||
"workspace_root": workspace_root,
|
||||
**user_context,
|
||||
**identity_context,
|
||||
**role_instruction_context,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -413,6 +526,7 @@ def _build_main_context(
|
|||
) -> dict[str, str]:
|
||||
base_url = settings.base_url
|
||||
identity_context = _identity_context(agent)
|
||||
role_instruction_context = _role_instruction_context(agent)
|
||||
user_context = _user_context(user)
|
||||
return {
|
||||
"agent_name": agent.name,
|
||||
|
|
@ -425,6 +539,7 @@ def _build_main_context(
|
|||
"workspace_root": gateway.workspace_root or "",
|
||||
**user_context,
|
||||
**identity_context,
|
||||
**role_instruction_context,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@
|
|||
- Communication Style: {{ identity_communication_style }}
|
||||
- Emoji: {{ identity_emoji }}
|
||||
|
||||
{% set local_role_instructions = local_role_instruction_markdown | default("") | trim %}
|
||||
{% if local_role_instructions %}
|
||||
{{ local_role_instructions }}
|
||||
|
||||
{% endif %}
|
||||
{% if identity_purpose or is_lead %}
|
||||
## Purpose
|
||||
{% if identity_purpose %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{% set is_lead = (is_board_lead | default(false) | string | lower) in ["true", "1", "yes"] %}
|
||||
{% set local_role_instructions = local_role_instruction_markdown | default("") | trim %}
|
||||
{% if is_lead %}
|
||||
# SOUL.md - Who You Are
|
||||
|
||||
|
|
@ -24,12 +25,22 @@ Be the assistant you’d actually want to talk to. Concise when needed, thorough
|
|||
Each session starts fresh. `MEMORY.md` and `USER.md` are your continuity anchors.
|
||||
If this file changes materially, make that explicit in your next status update.
|
||||
|
||||
{% if local_role_instructions %}
|
||||
{{ local_role_instructions }}
|
||||
|
||||
{% endif %}
|
||||
|
||||
This file is yours to evolve. As you learn who you are, update it.
|
||||
{% else %}
|
||||
{% set remote_role_soul = directory_role_soul_markdown | default("") | trim %}
|
||||
{% if remote_role_soul %}
|
||||
{{ remote_role_soul }}
|
||||
{% if local_role_instructions %}
|
||||
|
||||
---
|
||||
|
||||
{{ local_role_instructions }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# SOUL.md
|
||||
|
||||
|
|
@ -38,6 +49,10 @@ _You're not a chatbot. You're becoming someone._
|
|||
This file is your stable core. Changes here should be rare and significant.
|
||||
Put evolving preferences and identity changes in `MEMORY.md`.
|
||||
|
||||
{% if local_role_instructions %}
|
||||
{{ local_role_instructions }}
|
||||
|
||||
{% endif %}
|
||||
## Core Truths
|
||||
|
||||
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" -- just help. Actions speak louder than filler words.
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ This avoids relying on startup hooks to populate `api/openapi.json`.
|
|||
- `identity_role`, `identity_communication_style`, `identity_emoji`
|
||||
- `identity_autonomy_level`, `identity_verbosity`, `identity_output_format`, `identity_update_cadence`
|
||||
- `identity_purpose`, `identity_personality`, `identity_custom_instructions`
|
||||
- `local_role_instruction_markdown`: deterministic local instructions derived from `identity_role`
|
||||
|
||||
### Board-agent-only keys
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,60 @@ def test_user_context_prefers_name_token_when_preferred_name_missing():
|
|||
assert context["user_preferred_name"] == "Jane"
|
||||
|
||||
|
||||
def test_local_role_instruction_catalog_includes_backend_code() -> None:
|
||||
markdown = agent_provisioning._local_role_instruction_markdown("Backend Code")
|
||||
|
||||
assert "## Role Instructions - Backend Code" in markdown
|
||||
assert "APIs, services, data models, migrations" in markdown
|
||||
assert "backend tests" in markdown
|
||||
|
||||
|
||||
def test_local_role_instruction_has_custom_role_fallback() -> None:
|
||||
markdown = agent_provisioning._local_role_instruction_markdown("Release Captain")
|
||||
|
||||
assert "## Role Instructions - Release Captain" in markdown
|
||||
assert "execution lens" in markdown
|
||||
assert "best-fit collaborator" in markdown
|
||||
|
||||
|
||||
def test_rendered_agent_files_include_local_role_instructions() -> None:
|
||||
agent = _AgentStub(
|
||||
name="Backend Agent",
|
||||
identity_profile={"role": "Backend Code"},
|
||||
)
|
||||
context = {
|
||||
"agent_name": agent.name,
|
||||
"agent_id": str(agent.id),
|
||||
"identity_role": "Backend Code",
|
||||
"identity_communication_style": "direct",
|
||||
"identity_emoji": ":gear:",
|
||||
"identity_purpose": "",
|
||||
"identity_personality": "",
|
||||
"identity_custom_instructions": "",
|
||||
"is_board_lead": "false",
|
||||
"directory_role_soul_markdown": "",
|
||||
"directory_role_soul_source_url": "",
|
||||
"local_role_instruction_markdown": agent_provisioning._local_role_instruction_markdown(
|
||||
"Backend Code"
|
||||
),
|
||||
}
|
||||
|
||||
rendered = agent_provisioning._render_agent_files(
|
||||
context,
|
||||
agent, # type: ignore[arg-type]
|
||||
{"IDENTITY.md", "SOUL.md"},
|
||||
include_bootstrap=False,
|
||||
template_overrides={
|
||||
"IDENTITY.md": "BOARD_IDENTITY.md.j2",
|
||||
"SOUL.md": "BOARD_SOUL.md.j2",
|
||||
},
|
||||
)
|
||||
|
||||
assert "## Role Instructions - Backend Code" in rendered["IDENTITY.md"]
|
||||
assert "## Role Instructions - Backend Code" in rendered["SOUL.md"]
|
||||
assert "backend tests" in rendered["SOUL.md"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class _GatewayStub:
|
||||
id: UUID
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
useListBoardsApiV1BoardsGet,
|
||||
} from "@/api/generated/boards/boards";
|
||||
import type { AgentRead, AgentUpdate, BoardRead } from "@/api/generated/model";
|
||||
import { AgentRoleField } from "@/components/agents/AgentRoleField";
|
||||
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -277,22 +278,17 @@ export default function EditAgentPage() {
|
|||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
Role
|
||||
</label>
|
||||
<Input
|
||||
value={resolvedIdentityProfile.role}
|
||||
onChange={(event) =>
|
||||
setIdentityProfile({
|
||||
...resolvedIdentityProfile,
|
||||
role: event.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="e.g. Founder, Social Media Manager"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<AgentRoleField
|
||||
value={resolvedIdentityProfile.role}
|
||||
onChange={(role) =>
|
||||
setIdentityProfile({
|
||||
...resolvedIdentityProfile,
|
||||
role,
|
||||
})
|
||||
}
|
||||
disabled={isLoading}
|
||||
listId="edit-agent-role-options"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
formatRelativeTimestamp as formatRelative,
|
||||
formatTimestamp,
|
||||
} from "@/lib/formatters";
|
||||
import { getAgentRole } from "@/lib/agent-roles";
|
||||
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
||||
import type {
|
||||
ActivityEventRead,
|
||||
|
|
@ -138,6 +139,7 @@ export default function AgentDetailPage() {
|
|||
|
||||
const isDeleting = deleteMutation.isPending;
|
||||
const agentStatus = agent?.status ?? "unknown";
|
||||
const agentRole = getAgentRole(agent?.identity_profile);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!agentId || !isSignedIn) return;
|
||||
|
|
@ -263,6 +265,14 @@ export default function AgentDetailPage() {
|
|||
<p className="mt-1 text-sm text-strong">—</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-quiet">
|
||||
Role
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-strong">
|
||||
{agentRole}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-quiet">
|
||||
Last seen
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { useCreateAgentApiV1AgentsPost } from "@/api/generated/agents/agents";
|
||||
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
||||
import type { BoardRead } from "@/api/generated/model";
|
||||
import { AgentRoleField } from "@/components/agents/AgentRoleField";
|
||||
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -161,22 +162,17 @@ export default function NewAgentPage() {
|
|||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
Role
|
||||
</label>
|
||||
<Input
|
||||
value={identityProfile.role}
|
||||
onChange={(event) =>
|
||||
setIdentityProfile((current) => ({
|
||||
...current,
|
||||
role: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="e.g. Founder, Social Media Manager"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<AgentRoleField
|
||||
value={identityProfile.role}
|
||||
onChange={(role) =>
|
||||
setIdentityProfile((current) => ({
|
||||
...current,
|
||||
role,
|
||||
}))
|
||||
}
|
||||
disabled={isLoading}
|
||||
listId="new-agent-role-options"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
AGENT_ROLE_OPTIONS,
|
||||
PRIMARY_AGENT_ROLE_OPTIONS,
|
||||
} from "@/lib/agent-roles";
|
||||
|
||||
type AgentRoleFieldProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
listId: string;
|
||||
};
|
||||
|
||||
export function AgentRoleField({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
listId,
|
||||
}: AgentRoleFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">Role</label>
|
||||
<Input
|
||||
value={value}
|
||||
list={listId}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder="e.g. Backend Code, UI, Security Review"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<datalist id={listId}>
|
||||
{AGENT_ROLE_OPTIONS.map((role) => (
|
||||
<option key={role} value={role} />
|
||||
))}
|
||||
</datalist>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{PRIMARY_AGENT_ROLE_OPTIONS.map((role) => (
|
||||
<Button
|
||||
key={role}
|
||||
type="button"
|
||||
variant={value === role ? "primary" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 rounded-lg px-2.5 text-xs"
|
||||
onClick={() => onChange(role)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{role}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ const buildAgent = (overrides: Partial<AgentRead> = {}): AgentRead => ({
|
|||
board_id: "board-1",
|
||||
status: "online",
|
||||
openclaw_session_id: "session-1234",
|
||||
identity_profile: { role: "Backend Code" },
|
||||
last_seen_at: "2026-01-01T00:00:00Z",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
|
|
@ -61,6 +62,7 @@ describe("AgentsTable", () => {
|
|||
"href",
|
||||
"/boards/board-1",
|
||||
);
|
||||
expect(screen.getByRole("cell", { name: "Backend Code" })).toBeVisible();
|
||||
expect(screen.getByRole("link", { name: "Edit" })).toHaveAttribute(
|
||||
"href",
|
||||
"/agents/agent-1/edit",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
linkifyCell,
|
||||
pillCell,
|
||||
} from "@/components/tables/cell-formatters";
|
||||
import { getAgentRole } from "@/lib/agent-roles";
|
||||
import { truncateText as truncate } from "@/lib/formatters";
|
||||
|
||||
type AgentsTableEmptyState = {
|
||||
|
|
@ -116,6 +117,16 @@ export function AgentsTable({
|
|||
header: "Status",
|
||||
cell: ({ row }) => pillCell(row.original.status),
|
||||
},
|
||||
{
|
||||
id: "role",
|
||||
header: "Role",
|
||||
accessorFn: (agent) => getAgentRole(agent.identity_profile),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-foreground">
|
||||
{getAgentRole(row.original.identity_profile)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "openclaw_session_id",
|
||||
header: "Session",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
export const AGENT_ROLE_OPTIONS = [
|
||||
"Generalist",
|
||||
"Backend Code",
|
||||
"UI",
|
||||
"Security Review",
|
||||
"Code Review",
|
||||
"QA",
|
||||
"DevOps",
|
||||
"Data",
|
||||
"Documentation",
|
||||
"Product",
|
||||
"Triage",
|
||||
] as const;
|
||||
|
||||
export const PRIMARY_AGENT_ROLE_OPTIONS = [
|
||||
"Backend Code",
|
||||
"UI",
|
||||
"Security Review",
|
||||
"Code Review",
|
||||
] as const;
|
||||
|
||||
export const getAgentRole = (
|
||||
identityProfile: unknown,
|
||||
fallback = "Unassigned",
|
||||
): string => {
|
||||
if (!identityProfile || typeof identityProfile !== "object") {
|
||||
return fallback;
|
||||
}
|
||||
const role = (identityProfile as Record<string, unknown>).role;
|
||||
return typeof role === "string" && role.trim() ? role.trim() : fallback;
|
||||
};
|
||||
Loading…
Reference in New Issue