Initial commit
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
"""OPC Market — architecture package management."""
|
||||
|
||||
from .package_exporter import PackageExporter
|
||||
from .package_format import (
|
||||
ConflictReport,
|
||||
InstalledPackageInfo,
|
||||
OPCPackage,
|
||||
OPCPackageManifest,
|
||||
SandboxReport,
|
||||
)
|
||||
from .package_loader import PackageLoader
|
||||
from .sandbox_checker import SandboxChecker
|
||||
|
||||
__all__ = [
|
||||
"ConflictReport",
|
||||
"InstalledPackageInfo",
|
||||
"OPCPackage",
|
||||
"OPCPackageManifest",
|
||||
"PackageExporter",
|
||||
"PackageLoader",
|
||||
"SandboxChecker",
|
||||
"SandboxReport",
|
||||
]
|
||||
@@ -0,0 +1,708 @@
|
||||
"""Org-first architecture blueprint registry for OPC Market.
|
||||
|
||||
Blueprints define organization structure and optional work-item templates.
|
||||
Operational parameters (prompt_refs, runtime_policy,
|
||||
preferred_external_agent, RuntimePolicyConfig) are inferred at install-time
|
||||
from role hierarchy and template hints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from opc.core.config import (
|
||||
CommunicationPolicyConfig,
|
||||
HandoffPolicyConfig,
|
||||
MemoryPolicyConfig,
|
||||
ParallelPolicyConfig,
|
||||
ReviewPolicyConfig,
|
||||
RoleConfig,
|
||||
RoleRuntimePolicyConfig,
|
||||
RuntimePolicyConfig,
|
||||
slugify_organization_name,
|
||||
)
|
||||
from opc.market.package_format import InstalledPackageInfo
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Keywords used to infer preferred_external_agent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ENG_KEYWORDS: set[str] = {
|
||||
"code", "coding", "develop", "developer", "development",
|
||||
"engineer", "engineering", "implement", "implementation",
|
||||
"test", "testing", "qa", "quality assurance",
|
||||
"devops", "infrastructure", "ci/cd", "deploy", "deployment",
|
||||
"software", "backend", "frontend", "api", "debug", "debugging",
|
||||
"kubernetes", "cloud", "pipeline",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ArchitectureBlueprint — unified preset model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ArchitectureBlueprint(BaseModel):
|
||||
"""Unified architecture template — pure structure + display metadata.
|
||||
|
||||
Roles and work_item_templates contain only structural fields
|
||||
(id, name, responsibility, reports_to, can_spawn for roles;
|
||||
id, title, role_id, dependencies, parallel_group, gate for templates).
|
||||
|
||||
Operational config is inferred at install-time via
|
||||
``infer_collaboration_config()``.
|
||||
"""
|
||||
|
||||
# ── Display metadata (frontend Marketplace) ──────────────────────
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
category: str
|
||||
collaboration_pattern: str # descriptive label for UI filtering
|
||||
dag_summary: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
team_size: str = ""
|
||||
emoji: str = ""
|
||||
color: str = ""
|
||||
|
||||
# ── Pure structural definitions ──────────────────────────────────
|
||||
roles: list[dict[str, Any]]
|
||||
work_item_templates: list[dict[str, Any]]
|
||||
|
||||
def to_display_card(self) -> dict[str, Any]:
|
||||
"""Summary card for frontend browse — matches existing WS format."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"category": self.category,
|
||||
"tags": self.tags,
|
||||
"team_size": self.team_size,
|
||||
"emoji": self.emoji,
|
||||
"color": self.color,
|
||||
"roles_count": len(self.roles),
|
||||
"work_item_templates_count": len(self.work_item_templates),
|
||||
"gates_count": sum(
|
||||
1 for s in self.work_item_templates if s.get("gate")
|
||||
),
|
||||
"collaboration_pattern": self.collaboration_pattern,
|
||||
"dag_summary": self.dag_summary,
|
||||
}
|
||||
|
||||
def to_detail(self) -> dict[str, Any]:
|
||||
"""Full detail for frontend preview — matches existing WS format."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"category": self.category,
|
||||
"collaboration_pattern": self.collaboration_pattern,
|
||||
"dag_summary": self.dag_summary,
|
||||
"tags": self.tags,
|
||||
"team_size": self.team_size,
|
||||
"emoji": self.emoji,
|
||||
"color": self.color,
|
||||
"roles": [
|
||||
{
|
||||
"id": r["id"],
|
||||
"name": r["name"],
|
||||
"responsibility": r.get("responsibility", ""),
|
||||
"reports_to": r.get("reports_to", "owner"),
|
||||
"can_spawn": r.get("can_spawn", []),
|
||||
}
|
||||
for r in self.roles
|
||||
],
|
||||
"work_item_templates": [
|
||||
{
|
||||
"id": s["id"],
|
||||
"title": s.get("title", s["id"]),
|
||||
"role_id": s.get("role_id", ""),
|
||||
"dependencies": s.get("dependencies", []),
|
||||
"parallel_group": s.get("parallel_group"),
|
||||
"gate": (
|
||||
{
|
||||
"type": s["gate"]["type"],
|
||||
"reviewer_role": s["gate"].get("reviewer_role"),
|
||||
}
|
||||
if s.get("gate")
|
||||
else None
|
||||
),
|
||||
}
|
||||
for s in self.work_item_templates
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# YAML-backed built-in blueprints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_BUILTIN_PRESETS_DIR = Path(__file__).with_name("builtin_presets")
|
||||
|
||||
|
||||
def _expand_yaml_preset(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Expand small YAML conveniences into the ArchitectureBlueprint schema."""
|
||||
expanded = dict(data)
|
||||
toolsets = {
|
||||
str(name): list(tools or [])
|
||||
for name, tools in dict(expanded.pop("toolsets", {}) or {}).items()
|
||||
}
|
||||
roles: list[dict[str, Any]] = []
|
||||
for raw_role in list(expanded.get("roles", []) or []):
|
||||
role = dict(raw_role or {})
|
||||
toolset_name = str(role.pop("toolset", "") or "").strip()
|
||||
if toolset_name and not role.get("tools"):
|
||||
role["tools"] = list(toolsets.get(toolset_name, []))
|
||||
roles.append(role)
|
||||
expanded["roles"] = roles
|
||||
return expanded
|
||||
|
||||
|
||||
def load_architecture_presets_from_yaml(
|
||||
presets_dir: Path | None = None,
|
||||
) -> list[ArchitectureBlueprint]:
|
||||
"""Load built-in architecture presets from YAML files."""
|
||||
source_dir = Path(presets_dir or _BUILTIN_PRESETS_DIR)
|
||||
if not source_dir.is_dir():
|
||||
return []
|
||||
|
||||
presets: list[ArchitectureBlueprint] = []
|
||||
for path in sorted(source_dir.glob("*.yaml")):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Expected YAML mapping in architecture preset: {path}")
|
||||
presets.append(ArchitectureBlueprint.model_validate(_expand_yaml_preset(data)))
|
||||
return presets
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Collaboration inference — derives config from org topology
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def infer_collaboration_config(
|
||||
roles: list[dict[str, Any]],
|
||||
work_item_templates: list[dict[str, Any]],
|
||||
) -> tuple[list[RoleConfig], list[dict[str, Any]], RuntimePolicyConfig]:
|
||||
"""Infer full collaboration config from pure structural data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
roles : list of dicts with keys ``id, name, responsibility, reports_to, can_spawn``
|
||||
work_item_templates : optional template hints with keys ``id, title, role_id, dependencies, parallel_group, gate``
|
||||
|
||||
Returns
|
||||
-------
|
||||
(enriched_roles, enriched_work_item_templates, runtime_policy)
|
||||
Ready-to-install ``RoleConfig``, work-item template hints, and
|
||||
``RuntimePolicyConfig`` objects with operational fields filled.
|
||||
"""
|
||||
|
||||
# ── 1. Build topology graph ──────────────────────────────────────
|
||||
role_map: dict[str, dict[str, Any]] = {r["id"]: r for r in roles}
|
||||
|
||||
# children_of[role_id] = [child dicts that report_to this role]
|
||||
children_of: dict[str, list[dict[str, Any]]] = {r["id"]: [] for r in roles}
|
||||
for r in roles:
|
||||
parent = r.get("reports_to", "owner")
|
||||
if parent in children_of:
|
||||
children_of[parent].append(r)
|
||||
|
||||
# reviewer_roles = set of role_ids that appear as gate reviewers
|
||||
reviewer_roles: set[str] = set()
|
||||
for s in work_item_templates:
|
||||
gate = s.get("gate")
|
||||
if gate and gate.get("reviewer_role"):
|
||||
reviewer_roles.add(gate["reviewer_role"])
|
||||
|
||||
# Compute hierarchy depth per role
|
||||
def _depth(role_id: str, seen: set[str] | None = None) -> int:
|
||||
if seen is None:
|
||||
seen = set()
|
||||
if role_id in seen or role_id not in role_map:
|
||||
return 0
|
||||
seen.add(role_id)
|
||||
parent = role_map[role_id].get("reports_to", "owner")
|
||||
if parent == "owner" or parent not in role_map:
|
||||
return 0
|
||||
return 1 + _depth(parent, seen)
|
||||
|
||||
max_depth = max((_depth(r["id"]) for r in roles), default=0)
|
||||
|
||||
# ── 2. Classify each role and build RoleConfig ───────────────────
|
||||
enriched_roles: list[RoleConfig] = []
|
||||
|
||||
for r in roles:
|
||||
rid = r["id"]
|
||||
name = r.get("name", rid)
|
||||
responsibility = r.get("responsibility", "")
|
||||
reports_to = r.get("reports_to", "owner")
|
||||
can_spawn = r.get("can_spawn", [])
|
||||
children = children_of.get(rid, [])
|
||||
|
||||
is_coordinator = bool(children) or bool(can_spawn)
|
||||
is_reviewer = rid in reviewer_roles
|
||||
is_worker = not is_coordinator and not is_reviewer
|
||||
|
||||
# ── runtime_policy ──
|
||||
if is_coordinator:
|
||||
downstream = [c["id"] for c in children]
|
||||
if can_spawn:
|
||||
for candidate in can_spawn:
|
||||
if candidate not in downstream:
|
||||
downstream.append(candidate)
|
||||
runtime_policy = RoleRuntimePolicyConfig(
|
||||
execution_strategy="native",
|
||||
allowed_downstream_roles=downstream,
|
||||
)
|
||||
elif is_reviewer:
|
||||
runtime_policy = RoleRuntimePolicyConfig(
|
||||
execution_strategy="native",
|
||||
default_turn_type="review",
|
||||
)
|
||||
else:
|
||||
runtime_policy = RoleRuntimePolicyConfig(
|
||||
execution_strategy="auto",
|
||||
)
|
||||
|
||||
# ── prompt_refs ──
|
||||
prompt_parts = [f"You are the {name}. {responsibility}"]
|
||||
if is_coordinator:
|
||||
child_names = ", ".join(c.get("name", c["id"]) for c in children)
|
||||
if child_names:
|
||||
prompt_parts.append(
|
||||
f"Coordinate work with: {child_names}. "
|
||||
"Delegate tasks appropriately and aggregate results."
|
||||
)
|
||||
if is_reviewer:
|
||||
prompt_parts.append(
|
||||
"Review outputs for quality and correctness. "
|
||||
"Approve quality work or request changes."
|
||||
)
|
||||
if is_worker:
|
||||
prompt_parts.append(
|
||||
"Focus on delivering quality work within your area of expertise."
|
||||
)
|
||||
prompt_refs = [" ".join(prompt_parts), *list(r.get("prompt_refs") or [])]
|
||||
|
||||
# ── preferred_external_agent ──
|
||||
responsibility_lower = responsibility.lower()
|
||||
preferred_external_agent = r.get("preferred_external_agent")
|
||||
if any(kw in responsibility_lower for kw in _ENG_KEYWORDS):
|
||||
preferred_external_agent = preferred_external_agent or "claude_code"
|
||||
|
||||
raw_runtime_policy = r.get("runtime_policy")
|
||||
if isinstance(raw_runtime_policy, dict):
|
||||
runtime_policy = RoleRuntimePolicyConfig.model_validate({
|
||||
**runtime_policy.model_dump(),
|
||||
**raw_runtime_policy,
|
||||
})
|
||||
|
||||
enriched_roles.append(RoleConfig(
|
||||
id=rid,
|
||||
name=name,
|
||||
responsibility=responsibility,
|
||||
reports_to=reports_to,
|
||||
icon=r.get("icon"),
|
||||
can_spawn=can_spawn,
|
||||
tools=list(r.get("tools") or []),
|
||||
prompt_refs=prompt_refs,
|
||||
skill_refs=list(r.get("skill_refs") or []),
|
||||
runtime_policy=runtime_policy,
|
||||
preferred_external_agent=preferred_external_agent,
|
||||
capabilities=list(r.get("capabilities") or []),
|
||||
role_type=str(r.get("role_type") or ("coordinator" if is_coordinator else "reviewer" if is_reviewer else "worker")),
|
||||
))
|
||||
|
||||
# ── 3. Build enriched work-item template hints ───────────────────
|
||||
# Lookup: role_id → enriched RoleConfig
|
||||
role_config_map: dict[str, RoleConfig] = {rc.id: rc for rc in enriched_roles}
|
||||
|
||||
enriched_templates: list[dict[str, Any]] = []
|
||||
for s in work_item_templates:
|
||||
sid = s["id"]
|
||||
role_id = s.get("role_id", "")
|
||||
rc = role_config_map.get(role_id)
|
||||
role_name = rc.name if rc else role_id
|
||||
|
||||
# Inherit execution strategy from role
|
||||
exec_strategy = (
|
||||
rc.runtime_policy.execution_strategy if rc else "auto"
|
||||
)
|
||||
ext_agent = rc.preferred_external_agent if rc else None
|
||||
|
||||
raw_gate = s.get("gate")
|
||||
description = s.get(
|
||||
"description",
|
||||
f"{s.get('title', sid)} work item owned by {role_name}",
|
||||
)
|
||||
enriched_templates.append({
|
||||
"id": sid,
|
||||
"title": s.get("title", sid),
|
||||
"description": description,
|
||||
"role_id": role_id,
|
||||
"dependencies": list(s.get("dependencies", []) or []),
|
||||
"parallel_group": s.get("parallel_group"),
|
||||
"turn_type": str(s.get("turn_type") or "execute"),
|
||||
"execution_strategy": exec_strategy,
|
||||
"preferred_external_agent": ext_agent,
|
||||
"review_owner_role_id": str((raw_gate or {}).get("reviewer_role", "") or ""),
|
||||
"metadata": {
|
||||
"source": "market_work_item_template",
|
||||
"template_role_name": role_name,
|
||||
"gate": dict(raw_gate or {}),
|
||||
},
|
||||
})
|
||||
|
||||
# ── 4. Infer RuntimePolicyConfig from org/template characteristics ──
|
||||
has_gates = any(s.get("gate") for s in work_item_templates)
|
||||
has_parallel = any(s.get("parallel_group") for s in work_item_templates)
|
||||
|
||||
comm_cfg = CommunicationPolicyConfig(
|
||||
default_mode="broadcast" if max_depth > 3 else "dm",
|
||||
blocking_default=has_gates,
|
||||
allow_broadcast=True,
|
||||
)
|
||||
memory_cfg = MemoryPolicyConfig(
|
||||
include_role_memory=False,
|
||||
include_project_memory=False,
|
||||
recent_history_lines=12,
|
||||
)
|
||||
handoff_cfg = HandoffPolicyConfig(
|
||||
require_structured_handoff=True,
|
||||
require_ack=has_gates,
|
||||
include_risks=True,
|
||||
include_open_questions=True,
|
||||
)
|
||||
review_cfg = ReviewPolicyConfig(
|
||||
strict_gate_inference=has_gates,
|
||||
require_reviewer_role=has_gates,
|
||||
allow_human_override=True,
|
||||
)
|
||||
parallel_cfg = ParallelPolicyConfig(
|
||||
auto_dispatch=has_parallel,
|
||||
)
|
||||
|
||||
wf_policy = RuntimePolicyConfig(
|
||||
communication=comm_cfg,
|
||||
memory=memory_cfg,
|
||||
handoff=handoff_cfg,
|
||||
review=review_cfg,
|
||||
parallel=parallel_cfg,
|
||||
)
|
||||
|
||||
return enriched_roles, enriched_templates, wf_policy
|
||||
|
||||
|
||||
def _prefix_role_id(prefix: str, role_id: str) -> str:
|
||||
return f"{prefix}{role_id}" if prefix else role_id
|
||||
|
||||
|
||||
def apply_architecture_preset_to_config(
|
||||
config: Any,
|
||||
preset_id: str,
|
||||
*,
|
||||
strategy: str = "namespace",
|
||||
clear_existing: bool = True,
|
||||
organization_id: str | None = None,
|
||||
organization_name: str | None = None,
|
||||
) -> InstalledPackageInfo:
|
||||
"""Apply a built-in architecture preset as the active custom org.
|
||||
|
||||
This is the shared implementation for UI and CLI entry points. It keeps
|
||||
architecture presets org-first: roles define the hierarchy, and custom mode
|
||||
derives the runtime collaboration plan from that hierarchy at execution time.
|
||||
"""
|
||||
|
||||
preset = get_preset(preset_id)
|
||||
if preset is None:
|
||||
raise ValueError(f"Preset '{preset_id}' not found")
|
||||
if strategy not in {"namespace", "overwrite"}:
|
||||
raise ValueError("strategy must be 'namespace' or 'overwrite'")
|
||||
|
||||
prefix = f"{preset_id}:" if strategy == "namespace" else ""
|
||||
enriched_roles, enriched_templates, runtime_policy = infer_collaboration_config(
|
||||
preset.roles,
|
||||
preset.work_item_templates,
|
||||
)
|
||||
|
||||
if clear_existing:
|
||||
config.org.roles = []
|
||||
config.org.employees = []
|
||||
config.org.installed_packages = []
|
||||
|
||||
role_ids: list[str] = []
|
||||
for role in enriched_roles:
|
||||
role_copy = role.model_copy(deep=True)
|
||||
role_copy.id = _prefix_role_id(prefix, role_copy.id)
|
||||
if prefix and role_copy.reports_to and role_copy.reports_to != "owner":
|
||||
role_copy.reports_to = _prefix_role_id(prefix, role_copy.reports_to)
|
||||
if prefix:
|
||||
role_copy.can_spawn = [_prefix_role_id(prefix, role_id) for role_id in role_copy.can_spawn]
|
||||
role_copy.runtime_policy.allowed_downstream_roles = [
|
||||
_prefix_role_id(prefix, role_id)
|
||||
for role_id in role_copy.runtime_policy.allowed_downstream_roles
|
||||
]
|
||||
if role_copy.runtime_policy.review_role:
|
||||
role_copy.runtime_policy.review_role = _prefix_role_id(prefix, role_copy.runtime_policy.review_role)
|
||||
config.org.roles.append(role_copy)
|
||||
role_ids.append(role_copy.id)
|
||||
|
||||
work_item_template_ids = [
|
||||
_prefix_role_id(prefix, str(template.get("id", "") or "").strip())
|
||||
for template in enriched_templates
|
||||
if str(template.get("id", "") or "").strip()
|
||||
]
|
||||
|
||||
preset_org_name = str(organization_name or preset.name).strip()
|
||||
preset_org_id = str(organization_id or slugify_organization_name(preset_id)).strip()
|
||||
config.org.organization_id = preset_org_id
|
||||
config.org.organization_name = preset_org_name
|
||||
config.org.company_name = preset_org_name
|
||||
config.org.company_profile = "custom"
|
||||
if "custom" not in config.org.company_profiles:
|
||||
config.org.company_profiles.append("custom")
|
||||
|
||||
role_id_set = {role.id for role in config.org.roles}
|
||||
top_level_role_ids = [
|
||||
role.id
|
||||
for role in config.org.roles
|
||||
if role.id in role_ids and (role.reports_to == "owner" or role.reports_to not in role_id_set)
|
||||
]
|
||||
config.org.final_decider_role_id = top_level_role_ids[0] if len(top_level_role_ids) == 1 else None
|
||||
config.org.runtime_policies["custom"] = runtime_policy
|
||||
|
||||
info = InstalledPackageInfo(
|
||||
package_id=preset_id,
|
||||
name=preset.name,
|
||||
version="1.0.0",
|
||||
installed_at=datetime.now(timezone.utc).isoformat(),
|
||||
source_path="builtin",
|
||||
role_ids=role_ids,
|
||||
template_ids=work_item_template_ids,
|
||||
work_item_template_ids=work_item_template_ids,
|
||||
)
|
||||
config.org.installed_packages.append(info)
|
||||
return info
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Built-in architecture presets (pure structure)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ARCHITECTURE_PRESETS: list[ArchitectureBlueprint] = [
|
||||
*load_architecture_presets_from_yaml(),
|
||||
|
||||
# ── Startup Studio ────────────────────────────────────────────────
|
||||
ArchitectureBlueprint(
|
||||
id="startup-studio",
|
||||
name="Startup Studio",
|
||||
description="Lean full-stack startup team. CEO sets strategy, CTO drives tech decisions, engineers and designers build in parallel, QA validates.",
|
||||
category="startup",
|
||||
collaboration_pattern="hub_spoke",
|
||||
dag_summary="CEO\u2192CTO\u2192[Eng\u2225Des]\u2192QA\u2192Review\u2192Deploy",
|
||||
tags=["full-stack", "agile", "mvp", "small-team"],
|
||||
team_size="3-8",
|
||||
emoji="\U0001F680",
|
||||
color="#3498db",
|
||||
roles=[
|
||||
{"id": "ceo", "name": "CEO", "responsibility": "Product vision, strategy, and final decisions", "reports_to": "owner", "can_spawn": ["cto"]},
|
||||
{"id": "cto", "name": "CTO", "responsibility": "Technical architecture and engineering leadership", "reports_to": "ceo", "can_spawn": ["engineer", "designer"]},
|
||||
{"id": "engineer", "name": "Engineer", "responsibility": "Full-stack development and implementation", "reports_to": "cto", "can_spawn": []},
|
||||
{"id": "designer", "name": "Designer", "responsibility": "UI/UX design, prototyping, and user research", "reports_to": "cto", "can_spawn": []},
|
||||
{"id": "qa", "name": "QA Engineer", "responsibility": "Testing, quality assurance, and bug tracking", "reports_to": "cto", "can_spawn": []},
|
||||
],
|
||||
work_item_templates=[
|
||||
{"id": "planning", "title": "Planning", "role_id": "ceo", "dependencies": [], "parallel_group": None},
|
||||
{"id": "architecture", "title": "Architecture", "role_id": "cto", "dependencies": ["planning"], "parallel_group": None},
|
||||
{"id": "design", "title": "Design", "role_id": "designer", "dependencies": ["architecture"], "parallel_group": "build"},
|
||||
{"id": "development", "title": "Development", "role_id": "engineer", "dependencies": ["architecture"], "parallel_group": "build"},
|
||||
{"id": "testing", "title": "Testing", "role_id": "qa", "dependencies": ["design", "development"], "parallel_group": None},
|
||||
{"id": "review", "title": "Review", "role_id": "cto", "dependencies": ["testing"], "parallel_group": None, "gate": {"type": "review", "reviewer_role": "ceo"}},
|
||||
{"id": "deploy", "title": "Deploy", "role_id": "engineer", "dependencies": ["review"], "parallel_group": None},
|
||||
],
|
||||
),
|
||||
|
||||
# ── Enterprise Corp ───────────────────────────────────────────────
|
||||
ArchitectureBlueprint(
|
||||
id="enterprise-corp",
|
||||
name="Enterprise Corporation",
|
||||
description="Formal corporate hierarchy with department heads, approval gates, and compliance checkpoints. Suited for regulated industries.",
|
||||
category="enterprise",
|
||||
collaboration_pattern="hierarchical",
|
||||
dag_summary="PM\u2192CEO\u2713\u2192[Tech\u2225UX]\u2192Impl\u2192QA\u2192Security\u2713\u2192Prep\u2192Release\u2713",
|
||||
tags=["corporate", "compliance", "governance", "large-team"],
|
||||
team_size="10-50",
|
||||
emoji="\U0001F3E2",
|
||||
color="#2c3e50",
|
||||
roles=[
|
||||
{"id": "ceo", "name": "CEO", "responsibility": "Executive leadership, vision, investor relations", "reports_to": "owner", "can_spawn": ["vp_eng", "vp_product", "cfo"]},
|
||||
{"id": "vp_eng", "name": "VP Engineering", "responsibility": "Engineering organization leadership", "reports_to": "ceo", "can_spawn": ["tech_lead", "devops_lead"]},
|
||||
{"id": "vp_product", "name": "VP Product", "responsibility": "Product strategy and roadmap", "reports_to": "ceo", "can_spawn": ["product_manager", "ux_lead"]},
|
||||
{"id": "cfo", "name": "CFO", "responsibility": "Finance, budgeting, compliance", "reports_to": "ceo", "can_spawn": []},
|
||||
{"id": "tech_lead", "name": "Tech Lead", "responsibility": "Technical execution and code quality", "reports_to": "vp_eng", "can_spawn": ["engineer"]},
|
||||
{"id": "devops_lead", "name": "DevOps Lead", "responsibility": "Infrastructure, CI/CD, monitoring", "reports_to": "vp_eng", "can_spawn": []},
|
||||
{"id": "product_manager", "name": "Product Manager", "responsibility": "Feature specs, user stories, prioritization", "reports_to": "vp_product", "can_spawn": []},
|
||||
{"id": "ux_lead", "name": "UX Lead", "responsibility": "User experience design and research", "reports_to": "vp_product", "can_spawn": ["designer"]},
|
||||
{"id": "engineer", "name": "Software Engineer", "responsibility": "Development and implementation", "reports_to": "tech_lead", "can_spawn": []},
|
||||
{"id": "designer", "name": "UI Designer", "responsibility": "Visual design and prototyping", "reports_to": "ux_lead", "can_spawn": []},
|
||||
{"id": "qa_lead", "name": "QA Lead", "responsibility": "Quality assurance strategy and testing", "reports_to": "vp_eng", "can_spawn": []},
|
||||
],
|
||||
work_item_templates=[
|
||||
{"id": "requirements", "title": "Requirements", "role_id": "product_manager", "dependencies": [], "parallel_group": None},
|
||||
{"id": "exec_review", "title": "Executive Review", "role_id": "ceo", "dependencies": ["requirements"], "parallel_group": None, "gate": {"type": "approval", "reviewer_role": "ceo"}},
|
||||
{"id": "tech_design", "title": "Technical Design", "role_id": "tech_lead", "dependencies": ["exec_review"], "parallel_group": "design"},
|
||||
{"id": "ux_design", "title": "UX Design", "role_id": "ux_lead", "dependencies": ["exec_review"], "parallel_group": "design"},
|
||||
{"id": "implementation", "title": "Implementation", "role_id": "engineer", "dependencies": ["tech_design", "ux_design"], "parallel_group": None},
|
||||
{"id": "qa", "title": "Quality Assurance", "role_id": "qa_lead", "dependencies": ["implementation"], "parallel_group": None},
|
||||
{"id": "security_review", "title": "Security Review", "role_id": "devops_lead", "dependencies": ["qa"], "parallel_group": None, "gate": {"type": "review", "reviewer_role": "vp_eng"}},
|
||||
{"id": "preprod_deploy", "title": "Preprod Deploy", "role_id": "devops_lead", "dependencies": ["security_review"], "parallel_group": None},
|
||||
{"id": "release", "title": "Production Release", "role_id": "devops_lead", "dependencies": ["preprod_deploy"], "parallel_group": None, "gate": {"type": "approval", "reviewer_role": "vp_eng"}},
|
||||
],
|
||||
),
|
||||
|
||||
# ── Creative Agency ───────────────────────────────────────────────
|
||||
ArchitectureBlueprint(
|
||||
id="creative-agency",
|
||||
name="Creative Agency",
|
||||
description="Client-driven creative team. Account manager handles client relations, creative director sets the vision, specialists execute across disciplines.",
|
||||
category="agency",
|
||||
collaboration_pattern="review_loop",
|
||||
dag_summary="Brief\u2192Concept\u2192[Visual\u2225Copy]\u2192Build\u2192Review\u2713\u2192Client\u2713",
|
||||
tags=["creative", "client-work", "design", "marketing"],
|
||||
team_size="5-15",
|
||||
emoji="\U0001F3A8",
|
||||
color="#e74c3c",
|
||||
roles=[
|
||||
{"id": "account_manager", "name": "Account Manager", "responsibility": "Client relations, project scoping, deliverable tracking", "reports_to": "owner", "can_spawn": ["creative_director"]},
|
||||
{"id": "creative_director", "name": "Creative Director", "responsibility": "Creative vision, brand consistency, quality standards", "reports_to": "account_manager", "can_spawn": ["designer", "copywriter", "developer"]},
|
||||
{"id": "designer", "name": "Visual Designer", "responsibility": "Graphics, layouts, brand assets, UI mockups", "reports_to": "creative_director", "can_spawn": []},
|
||||
{"id": "copywriter", "name": "Copywriter", "responsibility": "Copy, content strategy, messaging, tone of voice", "reports_to": "creative_director", "can_spawn": []},
|
||||
{"id": "developer", "name": "Web Developer", "responsibility": "Frontend development, CMS, landing pages", "reports_to": "creative_director", "can_spawn": []},
|
||||
],
|
||||
work_item_templates=[
|
||||
{"id": "brief", "title": "Client Brief", "role_id": "account_manager", "dependencies": [], "parallel_group": None},
|
||||
{"id": "concept", "title": "Creative Concept", "role_id": "creative_director", "dependencies": ["brief"], "parallel_group": None},
|
||||
{"id": "visual_design", "title": "Visual Design", "role_id": "designer", "dependencies": ["concept"], "parallel_group": "create"},
|
||||
{"id": "copy", "title": "Copywriting", "role_id": "copywriter", "dependencies": ["concept"], "parallel_group": "create"},
|
||||
{"id": "build", "title": "Development", "role_id": "developer", "dependencies": ["visual_design", "copy"], "parallel_group": None},
|
||||
{"id": "creative_review", "title": "Creative Review", "role_id": "creative_director", "dependencies": ["build"], "parallel_group": None, "gate": {"type": "review", "reviewer_role": "creative_director"}},
|
||||
{"id": "client_approval", "title": "Client Approval", "role_id": "account_manager", "dependencies": ["creative_review"], "parallel_group": None, "gate": {"type": "approval", "reviewer_role": "account_manager"}},
|
||||
],
|
||||
),
|
||||
|
||||
# ── Research Lab ──────────────────────────────────────────────────
|
||||
ArchitectureBlueprint(
|
||||
id="research-lab",
|
||||
name="Research Lab",
|
||||
description="Academic-style research team. Principal investigator leads hypothesis-driven research with peer review and reproducibility checks.",
|
||||
category="research",
|
||||
collaboration_pattern="pipeline",
|
||||
dag_summary="Hypothesis\u2192LitReview\u2192Design\u2192[Data\u2225Experiment]\u2192Analysis\u2192PeerReview\u2713\u2192Publish",
|
||||
tags=["academic", "research", "data-science", "peer-review"],
|
||||
team_size="3-10",
|
||||
emoji="\U0001F52C",
|
||||
color="#9b59b6",
|
||||
roles=[
|
||||
{"id": "pi", "name": "Principal Investigator", "responsibility": "Research direction, hypothesis formulation, publication oversight", "reports_to": "owner", "can_spawn": ["researcher", "data_engineer"]},
|
||||
{"id": "researcher", "name": "Research Scientist", "responsibility": "Experiment design, analysis, paper writing", "reports_to": "pi", "can_spawn": []},
|
||||
{"id": "data_engineer", "name": "Data Engineer", "responsibility": "Data pipelines, infrastructure, reproducibility", "reports_to": "pi", "can_spawn": []},
|
||||
{"id": "reviewer", "name": "Peer Reviewer", "responsibility": "Critical review, methodology validation, feedback", "reports_to": "pi", "can_spawn": []},
|
||||
],
|
||||
work_item_templates=[
|
||||
{"id": "hypothesis", "title": "Hypothesis", "role_id": "pi", "dependencies": [], "parallel_group": None},
|
||||
{"id": "lit_review", "title": "Literature Review", "role_id": "researcher", "dependencies": ["hypothesis"], "parallel_group": None},
|
||||
{"id": "experiment_design", "title": "Experiment Design", "role_id": "researcher", "dependencies": ["lit_review"], "parallel_group": None},
|
||||
{"id": "data_collection", "title": "Data Pipeline", "role_id": "data_engineer", "dependencies": ["experiment_design"], "parallel_group": "exec"},
|
||||
{"id": "experiment", "title": "Run Experiment", "role_id": "researcher", "dependencies": ["experiment_design"], "parallel_group": "exec"},
|
||||
{"id": "analysis", "title": "Analysis", "role_id": "researcher", "dependencies": ["data_collection", "experiment"], "parallel_group": None},
|
||||
{"id": "peer_review", "title": "Peer Review", "role_id": "reviewer", "dependencies": ["analysis"], "parallel_group": None, "gate": {"type": "review", "reviewer_role": "pi"}},
|
||||
{"id": "publication", "title": "Publication", "role_id": "pi", "dependencies": ["peer_review"], "parallel_group": None},
|
||||
],
|
||||
),
|
||||
|
||||
# ── DevOps Pipeline ───────────────────────────────────────────────
|
||||
ArchitectureBlueprint(
|
||||
id="devops-pipeline",
|
||||
name="DevOps Pipeline",
|
||||
description="Infrastructure-focused team optimized for CI/CD, monitoring, and rapid deployment cycles with SRE practices.",
|
||||
category="engineering",
|
||||
collaboration_pattern="pipeline",
|
||||
dag_summary="Plan\u2192Dev\u2192CI\u2192[Security\u2225Tests]\u2192Prep\u2192Canary\u2713\u2192Production",
|
||||
tags=["devops", "sre", "infrastructure", "automation"],
|
||||
team_size="4-12",
|
||||
emoji="\u2699\uFE0F",
|
||||
color="#27ae60",
|
||||
roles=[
|
||||
{"id": "sre_lead", "name": "SRE Lead", "responsibility": "Reliability strategy, incident response, SLO management", "reports_to": "owner", "can_spawn": ["platform_eng", "security_eng"]},
|
||||
{"id": "platform_eng", "name": "Platform Engineer", "responsibility": "Infrastructure as code, Kubernetes, cloud architecture", "reports_to": "sre_lead", "can_spawn": []},
|
||||
{"id": "security_eng", "name": "Security Engineer", "responsibility": "Security audits, vulnerability scanning, compliance", "reports_to": "sre_lead", "can_spawn": []},
|
||||
{"id": "developer", "name": "Developer", "responsibility": "Application development and feature delivery", "reports_to": "sre_lead", "can_spawn": []},
|
||||
],
|
||||
work_item_templates=[
|
||||
{"id": "plan", "title": "Sprint Planning", "role_id": "sre_lead", "dependencies": [], "parallel_group": None},
|
||||
{"id": "develop", "title": "Development", "role_id": "developer", "dependencies": ["plan"], "parallel_group": None},
|
||||
{"id": "ci", "title": "CI Pipeline", "role_id": "platform_eng", "dependencies": ["develop"], "parallel_group": None},
|
||||
{"id": "security_scan", "title": "Security Scan", "role_id": "security_eng", "dependencies": ["ci"], "parallel_group": "validate"},
|
||||
{"id": "integration_test", "title": "Integration Tests", "role_id": "platform_eng", "dependencies": ["ci"], "parallel_group": "validate"},
|
||||
{"id": "preprod_deploy", "title": "Preprod Deploy", "role_id": "platform_eng", "dependencies": ["security_scan", "integration_test"], "parallel_group": None},
|
||||
{"id": "canary", "title": "Canary Release", "role_id": "sre_lead", "dependencies": ["preprod_deploy"], "parallel_group": None, "gate": {"type": "approval", "reviewer_role": "sre_lead"}},
|
||||
{"id": "production", "title": "Production", "role_id": "platform_eng", "dependencies": ["canary"], "parallel_group": None},
|
||||
],
|
||||
),
|
||||
|
||||
# ── Content Studio ────────────────────────────────────────────────
|
||||
ArchitectureBlueprint(
|
||||
id="content-studio",
|
||||
name="Content Studio",
|
||||
description="Content creation pipeline for blogs, social media, and documentation. Editor-in-chief oversees quality and publishing cadence.",
|
||||
category="media",
|
||||
collaboration_pattern="pipeline",
|
||||
dag_summary="Topic\u2192[Research\u2225SEO]\u2192Draft\u2192EditReview\u2713\u2192Publish\u2192Distribute",
|
||||
tags=["content", "writing", "social-media", "publishing"],
|
||||
team_size="3-8",
|
||||
emoji="\U0001F4DD",
|
||||
color="#f39c12",
|
||||
roles=[
|
||||
{"id": "editor_in_chief", "name": "Editor-in-Chief", "responsibility": "Editorial strategy, content calendar, quality standards", "reports_to": "owner", "can_spawn": ["writer", "seo_specialist"]},
|
||||
{"id": "writer", "name": "Content Writer", "responsibility": "Research, drafting, and revising articles", "reports_to": "editor_in_chief", "can_spawn": []},
|
||||
{"id": "seo_specialist", "name": "SEO Specialist", "responsibility": "Keyword research, optimization, analytics", "reports_to": "editor_in_chief", "can_spawn": []},
|
||||
{"id": "social_manager", "name": "Social Media Manager", "responsibility": "Distribution, engagement, cross-promotion", "reports_to": "editor_in_chief", "can_spawn": []},
|
||||
],
|
||||
work_item_templates=[
|
||||
{"id": "topic_planning", "title": "Topic Planning", "role_id": "editor_in_chief", "dependencies": [], "parallel_group": None},
|
||||
{"id": "research", "title": "Research & Outline", "role_id": "writer", "dependencies": ["topic_planning"], "parallel_group": None},
|
||||
{"id": "seo_research", "title": "SEO Research", "role_id": "seo_specialist", "dependencies": ["topic_planning"], "parallel_group": None},
|
||||
{"id": "drafting", "title": "Drafting", "role_id": "writer", "dependencies": ["research", "seo_research"], "parallel_group": None},
|
||||
{"id": "editorial_review", "title": "Editorial Review", "role_id": "editor_in_chief", "dependencies": ["drafting"], "parallel_group": None, "gate": {"type": "review", "reviewer_role": "editor_in_chief"}},
|
||||
{"id": "publish", "title": "Publish", "role_id": "editor_in_chief", "dependencies": ["editorial_review"], "parallel_group": None},
|
||||
{"id": "distribute", "title": "Social Distribution", "role_id": "social_manager", "dependencies": ["publish"], "parallel_group": None},
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_all_presets() -> list[ArchitectureBlueprint]:
|
||||
"""Return all built-in architecture presets."""
|
||||
return ARCHITECTURE_PRESETS
|
||||
|
||||
|
||||
def get_preset(preset_id: str) -> ArchitectureBlueprint | None:
|
||||
"""Return a single preset by ID."""
|
||||
for p in ARCHITECTURE_PRESETS:
|
||||
if p.id == preset_id:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def get_preset_categories() -> list[str]:
|
||||
"""Return unique categories across all presets."""
|
||||
return sorted({p.category for p in ARCHITECTURE_PRESETS})
|
||||
@@ -0,0 +1,404 @@
|
||||
id: vc-investment-firm
|
||||
name: VC Investment Firm
|
||||
description: >
|
||||
Venture-capital investment organization for sector mapping, startup sourcing,
|
||||
technical and commercial due diligence, investment committee debate, and
|
||||
memo/PPT delivery.
|
||||
category: investment
|
||||
collaboration_pattern: hierarchical
|
||||
dag_summary: Brief->Research/Sourcing->DD->Bull/Bear IC->Top 3->Memo/PPT
|
||||
tags:
|
||||
- vc
|
||||
- investment
|
||||
- due-diligence
|
||||
- startup-scouting
|
||||
- market-research
|
||||
team_size: 8-21
|
||||
emoji: ''
|
||||
color: '#0f766e'
|
||||
toolsets:
|
||||
coordination:
|
||||
- file_read
|
||||
- file_search
|
||||
- list_dir
|
||||
- todo_write
|
||||
- todo_read
|
||||
- web_search
|
||||
- web_fetch
|
||||
research:
|
||||
- file_read
|
||||
- file_search
|
||||
- list_dir
|
||||
- todo_write
|
||||
- todo_read
|
||||
- web_search
|
||||
- web_fetch
|
||||
- browser_navigate
|
||||
- browser_navigate_back
|
||||
- browser_snapshot
|
||||
- browser_wait_for
|
||||
- browser_scroll
|
||||
- browser_take_screenshot
|
||||
analysis:
|
||||
- file_read
|
||||
- file_search
|
||||
- list_dir
|
||||
- todo_write
|
||||
- todo_read
|
||||
- web_search
|
||||
- web_fetch
|
||||
- browser_navigate
|
||||
- browser_navigate_back
|
||||
- browser_snapshot
|
||||
- browser_wait_for
|
||||
- browser_scroll
|
||||
- browser_take_screenshot
|
||||
- shell_exec
|
||||
- file_write
|
||||
- file_edit
|
||||
delivery:
|
||||
- shell_exec
|
||||
- file_read
|
||||
- file_write
|
||||
- file_edit
|
||||
- file_search
|
||||
- list_dir
|
||||
- web_search
|
||||
- web_fetch
|
||||
- todo_write
|
||||
- todo_read
|
||||
- browser_navigate
|
||||
- browser_snapshot
|
||||
- browser_take_screenshot
|
||||
roles:
|
||||
- id: managing_partner
|
||||
name: Managing Partner
|
||||
responsibility: Defines the investment mandate, target stage, sector focus, decision criteria, and final portfolio recommendation.
|
||||
reports_to: owner
|
||||
can_spawn:
|
||||
- investment_director
|
||||
- due_diligence_lead
|
||||
- investment_committee
|
||||
- report_delivery_lead
|
||||
icon: leader
|
||||
toolset: coordination
|
||||
prompt_refs:
|
||||
- Keep the team focused on investment decisions rather than raw research accumulation.
|
||||
- Final output must identify Top 3 companies, investment rationale, key risks, entry path, suggested stage, and follow-up plan.
|
||||
- id: investment_director
|
||||
name: Investment Director
|
||||
responsibility: Translates the user request into an investment research framework, screening criteria, task split, and candidate company shortlist.
|
||||
reports_to: managing_partner
|
||||
can_spawn:
|
||||
- sector_analyst
|
||||
- market_researcher
|
||||
- startup_scout
|
||||
- competitive_analyst
|
||||
- news_signal_analyst
|
||||
icon: strategy
|
||||
toolset: coordination
|
||||
prompt_refs:
|
||||
- 'Produce a clear VC research framework: market thesis, screening standard, candidate pool logic, and scoring rubric.'
|
||||
- Ask scouts and analysts for evidence tables, not generic summaries.
|
||||
- id: sector_analyst
|
||||
name: Sector Analyst
|
||||
responsibility: Evaluates whether the target sector can compound over the next 3-5 years, including demand drivers, inflection points, and adoption constraints.
|
||||
reports_to: investment_director
|
||||
icon: analytics
|
||||
toolset: research
|
||||
- id: market_researcher
|
||||
name: Market Researcher
|
||||
responsibility: Quantifies TAM/SAM/SOM, buyer budgets, growth rates, adoption curves, and comparable public or private market signals.
|
||||
reports_to: investment_director
|
||||
icon: analytics
|
||||
toolset: research
|
||||
- id: startup_scout
|
||||
name: Startup Scout
|
||||
responsibility: Builds a broad candidate pool with founding date, headquarters, funding stage, amount raised, investors, product, customers, news, website, LinkedIn, Crunchbase, PitchBook, GitHub, and source URLs.
|
||||
reports_to: investment_director
|
||||
icon: target
|
||||
toolset: research
|
||||
prompt_refs:
|
||||
- 'Prioritize breadth first: gather a defensible longlist before ranking.'
|
||||
- Separate verified facts from inferred signals and keep source URLs next to every company row.
|
||||
- id: competitive_analyst
|
||||
name: Competitive Analyst
|
||||
responsibility: Maps incumbents, startup competitors, substitute technologies, market positioning, moat strength, differentiation, and big-tech pressure.
|
||||
reports_to: investment_director
|
||||
icon: team
|
||||
toolset: research
|
||||
- id: news_signal_analyst
|
||||
name: News & Signal Analyst
|
||||
responsibility: Tracks recent market signals, customer wins, launches, layoffs, regulatory movement, hiring velocity, GitHub activity, and investor interest.
|
||||
reports_to: investment_director
|
||||
icon: marketing
|
||||
toolset: research
|
||||
- id: due_diligence_lead
|
||||
name: Due Diligence Lead
|
||||
responsibility: Turns the shortlist into a diligence plan, coordinates specialist checks, resolves contradictions, and synthesizes company-level conviction.
|
||||
reports_to: managing_partner
|
||||
can_spawn:
|
||||
- technical_dd_analyst
|
||||
- business_dd_analyst
|
||||
- financial_analyst
|
||||
- risk_legal_analyst
|
||||
icon: clipboard
|
||||
toolset: coordination
|
||||
prompt_refs:
|
||||
- Force each diligence stream to give a verdict, confidence level, and disconfirming evidence.
|
||||
- Maintain a comparable scorecard across all shortlisted companies.
|
||||
- id: technical_dd_analyst
|
||||
name: Technical DD Analyst
|
||||
responsibility: Assesses technical moat, architecture credibility, benchmarks, patents, papers, open-source projects, GitHub traction, team technical depth, and scalability.
|
||||
reports_to: due_diligence_lead
|
||||
icon: code
|
||||
toolset: analysis
|
||||
- id: business_dd_analyst
|
||||
name: Business DD Analyst
|
||||
responsibility: Evaluates target customers, sales motion, pricing model, evidence of ARR or revenue, deployment friction, customer concentration, and go-to-market defensibility.
|
||||
reports_to: due_diligence_lead
|
||||
icon: strategy
|
||||
toolset: research
|
||||
- id: financial_analyst
|
||||
name: Financial Analyst
|
||||
responsibility: Analyzes financing history, investors, valuation signals, burn-rate implications, round dynamics, comparable valuations, exit routes, and upside risk/reward.
|
||||
reports_to: due_diligence_lead
|
||||
icon: analytics
|
||||
toolset: analysis
|
||||
- id: risk_legal_analyst
|
||||
name: Risk & Legal Analyst
|
||||
responsibility: Finds privacy, open-source license, supply-chain, geopolitical, customer concentration, regulatory, IP, and founder-stability risks.
|
||||
reports_to: due_diligence_lead
|
||||
icon: security
|
||||
toolset: research
|
||||
prompt_refs:
|
||||
- Actively search for reasons not to invest; do not let opportunity framing hide material risks.
|
||||
- id: investment_committee
|
||||
name: Investment Committee Chair
|
||||
responsibility: Runs structured bull-case and bear-case review, compares evidence quality, and prepares the decision package for the Managing Partner.
|
||||
reports_to: managing_partner
|
||||
can_spawn:
|
||||
- bull_case_reviewer
|
||||
- bear_case_reviewer
|
||||
- final_decision_reviewer
|
||||
icon: team
|
||||
toolset: coordination
|
||||
- id: bull_case_reviewer
|
||||
name: Bull Case Reviewer
|
||||
responsibility: Builds the strongest evidence-backed case for why each shortlisted company should be funded now.
|
||||
reports_to: investment_committee
|
||||
icon: idea
|
||||
toolset: research
|
||||
- id: bear_case_reviewer
|
||||
name: Bear Case Reviewer
|
||||
responsibility: Builds the strongest evidence-backed case against investing, including timing, valuation, technical, market, and execution objections.
|
||||
reports_to: investment_committee
|
||||
icon: bug
|
||||
toolset: research
|
||||
- id: final_decision_reviewer
|
||||
name: Final Decision Reviewer
|
||||
responsibility: Reconciles bull and bear cases, ranks companies, explains confidence, and flags follow-up diligence before partner approval.
|
||||
reports_to: investment_committee
|
||||
icon: clipboard
|
||||
toolset: coordination
|
||||
- id: report_delivery_lead
|
||||
name: Report & Delivery Lead
|
||||
responsibility: 'Owns final deliverables: investment memo, scorecard, market map, competitive matrix, financing timeline, PPT, data table, and charts.'
|
||||
reports_to: managing_partner
|
||||
can_spawn:
|
||||
- data_analyst
|
||||
- investment_memo_writer
|
||||
- ppt_designer
|
||||
- visualization_specialist
|
||||
icon: layout
|
||||
toolset: coordination
|
||||
- id: data_analyst
|
||||
name: Data Analyst
|
||||
responsibility: Normalizes company data, computes scorecards, creates comparable tables, and prepares chart-ready datasets.
|
||||
reports_to: report_delivery_lead
|
||||
icon: database
|
||||
toolset: analysis
|
||||
- id: investment_memo_writer
|
||||
name: Investment Memo Writer
|
||||
responsibility: Writes a partner-quality investment memo with thesis, market, company analysis, diligence evidence, risks, valuation view, and recommendation.
|
||||
reports_to: report_delivery_lead
|
||||
icon: writing
|
||||
toolset: delivery
|
||||
- id: ppt_designer
|
||||
name: PPT Designer
|
||||
responsibility: Turns the investment memo into a concise VC-style presentation with clear hierarchy, scorecards, market maps, and decision slides.
|
||||
reports_to: report_delivery_lead
|
||||
icon: design
|
||||
toolset: delivery
|
||||
- id: visualization_specialist
|
||||
name: Visualization Specialist
|
||||
responsibility: Creates market maps, competitor matrices, financing timelines, scoring charts, and visual summaries for the final report and PPT.
|
||||
reports_to: report_delivery_lead
|
||||
icon: analytics
|
||||
toolset: analysis
|
||||
work_item_templates:
|
||||
- id: investment_brief
|
||||
title: Investment Brief
|
||||
role_id: managing_partner
|
||||
dependencies: []
|
||||
parallel_group: null
|
||||
- id: research_framework
|
||||
title: Research Framework
|
||||
role_id: investment_director
|
||||
dependencies:
|
||||
- investment_brief
|
||||
parallel_group: null
|
||||
- id: sector_thesis
|
||||
title: Sector Thesis
|
||||
role_id: sector_analyst
|
||||
dependencies:
|
||||
- research_framework
|
||||
parallel_group: research
|
||||
- id: market_sizing
|
||||
title: Market Sizing
|
||||
role_id: market_researcher
|
||||
dependencies:
|
||||
- research_framework
|
||||
parallel_group: research
|
||||
- id: company_sourcing
|
||||
title: Company Sourcing
|
||||
role_id: startup_scout
|
||||
dependencies:
|
||||
- research_framework
|
||||
parallel_group: sourcing
|
||||
- id: signal_scan
|
||||
title: News & Signal Scan
|
||||
role_id: news_signal_analyst
|
||||
dependencies:
|
||||
- research_framework
|
||||
parallel_group: sourcing
|
||||
- id: competitive_map
|
||||
title: Competitive Map
|
||||
role_id: competitive_analyst
|
||||
dependencies:
|
||||
- sector_thesis
|
||||
- company_sourcing
|
||||
parallel_group: null
|
||||
- id: candidate_shortlist
|
||||
title: Candidate Shortlist
|
||||
role_id: investment_director
|
||||
dependencies:
|
||||
- sector_thesis
|
||||
- market_sizing
|
||||
- company_sourcing
|
||||
- signal_scan
|
||||
- competitive_map
|
||||
parallel_group: null
|
||||
gate:
|
||||
type: review
|
||||
reviewer_role: managing_partner
|
||||
- id: dd_plan
|
||||
title: Diligence Plan
|
||||
role_id: due_diligence_lead
|
||||
dependencies:
|
||||
- candidate_shortlist
|
||||
parallel_group: null
|
||||
- id: technical_dd
|
||||
title: Technical DD
|
||||
role_id: technical_dd_analyst
|
||||
dependencies:
|
||||
- dd_plan
|
||||
parallel_group: diligence
|
||||
- id: business_dd
|
||||
title: Business DD
|
||||
role_id: business_dd_analyst
|
||||
dependencies:
|
||||
- dd_plan
|
||||
parallel_group: diligence
|
||||
- id: financial_dd
|
||||
title: Financial DD
|
||||
role_id: financial_analyst
|
||||
dependencies:
|
||||
- dd_plan
|
||||
parallel_group: diligence
|
||||
- id: risk_legal_dd
|
||||
title: Risk & Legal DD
|
||||
role_id: risk_legal_analyst
|
||||
dependencies:
|
||||
- dd_plan
|
||||
parallel_group: diligence
|
||||
- id: dd_synthesis
|
||||
title: DD Synthesis
|
||||
role_id: due_diligence_lead
|
||||
dependencies:
|
||||
- technical_dd
|
||||
- business_dd
|
||||
- financial_dd
|
||||
- risk_legal_dd
|
||||
parallel_group: null
|
||||
gate:
|
||||
type: review
|
||||
reviewer_role: investment_director
|
||||
- id: bull_case
|
||||
title: Bull Case
|
||||
role_id: bull_case_reviewer
|
||||
dependencies:
|
||||
- dd_synthesis
|
||||
parallel_group: committee
|
||||
- id: bear_case
|
||||
title: Bear Case
|
||||
role_id: bear_case_reviewer
|
||||
dependencies:
|
||||
- dd_synthesis
|
||||
parallel_group: committee
|
||||
- id: decision_review
|
||||
title: Decision Review
|
||||
role_id: final_decision_reviewer
|
||||
dependencies:
|
||||
- bull_case
|
||||
- bear_case
|
||||
parallel_group: null
|
||||
gate:
|
||||
type: review
|
||||
reviewer_role: investment_committee
|
||||
- id: final_investment_decision
|
||||
title: Final Investment Decision
|
||||
role_id: managing_partner
|
||||
dependencies:
|
||||
- decision_review
|
||||
parallel_group: null
|
||||
gate:
|
||||
type: approval
|
||||
reviewer_role: managing_partner
|
||||
- id: dataset_scoring
|
||||
title: Dataset & Scorecard
|
||||
role_id: data_analyst
|
||||
dependencies:
|
||||
- dd_synthesis
|
||||
parallel_group: delivery
|
||||
- id: investment_memo
|
||||
title: Investment Memo
|
||||
role_id: investment_memo_writer
|
||||
dependencies:
|
||||
- decision_review
|
||||
- dataset_scoring
|
||||
parallel_group: delivery
|
||||
- id: visualizations
|
||||
title: Visualizations
|
||||
role_id: visualization_specialist
|
||||
dependencies:
|
||||
- dataset_scoring
|
||||
parallel_group: delivery
|
||||
- id: pitch_deck
|
||||
title: PPT Deck
|
||||
role_id: ppt_designer
|
||||
dependencies:
|
||||
- investment_memo
|
||||
- visualizations
|
||||
parallel_group: null
|
||||
- id: final_delivery
|
||||
title: Final VC Deliverables
|
||||
role_id: report_delivery_lead
|
||||
dependencies:
|
||||
- final_investment_decision
|
||||
- investment_memo
|
||||
- pitch_deck
|
||||
parallel_group: null
|
||||
gate:
|
||||
type: approval
|
||||
reviewer_role: managing_partner
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Export the current org configuration as an .opcpkg package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import yaml
|
||||
|
||||
from .package_format import OPCPackage, OPCPackageManifest, PackageAuthor, PackageContents
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from opc.core.config import OPCConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PackageExporter:
|
||||
"""Exports the current organisation as a self-contained .opcpkg directory."""
|
||||
|
||||
def __init__(self, config: OPCConfig, opc_home: Path) -> None:
|
||||
self.config = config
|
||||
self.opc_home = opc_home
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def export_current(
|
||||
self,
|
||||
package_id: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
version: str = "1.0.0",
|
||||
author_name: str = "",
|
||||
author_github: str = "",
|
||||
) -> OPCPackage:
|
||||
"""Build an OPCPackage from the live org config."""
|
||||
org = self.config.org
|
||||
|
||||
roles = [r.model_dump() for r in org.roles]
|
||||
employees = [e.model_dump() for e in org.employees]
|
||||
templates_by_id = {
|
||||
str(getattr(template, "id", "") or ""): template
|
||||
for template in list(getattr(org, "talent_templates", []) or [])
|
||||
if str(getattr(template, "id", "") or "")
|
||||
}
|
||||
employee_template_ids = {
|
||||
str(employee.get("template_id", "") or "").strip()
|
||||
for employee in employees
|
||||
if str(employee.get("template_id", "") or "").strip()
|
||||
}
|
||||
try:
|
||||
from opc.layer2_organization.talent_market import TalentMarket
|
||||
|
||||
catalog = {template.id: template for template in TalentMarket(self.opc_home, self.config).list_available_templates()}
|
||||
for template_id in employee_template_ids:
|
||||
if template_id in catalog:
|
||||
templates_by_id.setdefault(template_id, catalog[template_id])
|
||||
except Exception:
|
||||
pass
|
||||
templates = [template.model_dump() for template in templates_by_id.values()]
|
||||
|
||||
# Serialize runtime policy for the current profile
|
||||
wf_policy = None
|
||||
profile = org.company_profile
|
||||
if profile in org.runtime_policies:
|
||||
wf_policy = org.runtime_policies[profile].model_dump()
|
||||
|
||||
# Collect referenced prompt files
|
||||
prompt_contents = self._collect_prompts(roles, templates, employees)
|
||||
|
||||
manifest = OPCPackageManifest(
|
||||
id=package_id,
|
||||
name=name,
|
||||
description=description,
|
||||
version=version,
|
||||
author=PackageAuthor(name=author_name, github=author_github),
|
||||
contents=PackageContents(
|
||||
roles=len(roles),
|
||||
work_item_templates=0,
|
||||
gates=0,
|
||||
prompts=len(prompt_contents),
|
||||
),
|
||||
)
|
||||
|
||||
return OPCPackage(
|
||||
manifest=manifest,
|
||||
roles=roles,
|
||||
runtime_policy=wf_policy,
|
||||
talent_templates=templates,
|
||||
employees=employees,
|
||||
prompt_contents=prompt_contents,
|
||||
readme=self._generate_readme(manifest, roles, None),
|
||||
)
|
||||
|
||||
def write_to_path(self, package: OPCPackage, out_dir: Path) -> Path:
|
||||
"""Write an OPCPackage to disk as a .opcpkg directory."""
|
||||
pkg_dir = out_dir / f"{package.manifest.id}.opcpkg"
|
||||
pkg_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# manifest.yaml
|
||||
with open(pkg_dir / "manifest.yaml", "w", encoding="utf-8") as f:
|
||||
yaml.dump(package.manifest.model_dump(), f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
# org_config.yaml
|
||||
org_data: dict = {
|
||||
"roles": package.roles,
|
||||
"talent_templates": package.talent_templates,
|
||||
"employees": package.employees,
|
||||
"work_item_templates": package.work_item_templates,
|
||||
}
|
||||
if package.runtime_policy:
|
||||
org_data["runtime_policy"] = package.runtime_policy
|
||||
with open(pkg_dir / "org_config.yaml", "w", encoding="utf-8") as f:
|
||||
yaml.dump(org_data, f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
# prompts/
|
||||
if package.prompt_contents:
|
||||
prompts_dir = pkg_dir / "prompts"
|
||||
prompts_dir.mkdir(exist_ok=True)
|
||||
for filename, content in package.prompt_contents.items():
|
||||
with open(prompts_dir / filename, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
# README.md
|
||||
with open(pkg_dir / "README.md", "w", encoding="utf-8") as f:
|
||||
f.write(package.readme)
|
||||
|
||||
logger.info("Exported package %s to %s", package.manifest.id, pkg_dir)
|
||||
return pkg_dir
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _collect_prompts(
|
||||
self,
|
||||
roles: list[dict],
|
||||
templates: list[dict],
|
||||
employees: list[dict],
|
||||
) -> dict[str, str]:
|
||||
"""Read all referenced prompt files and return {filename: content}."""
|
||||
refs: set[str] = set()
|
||||
for r in roles:
|
||||
refs.update(r.get("prompt_refs") or [])
|
||||
for t in templates:
|
||||
ref = t.get("prompt_ref", "")
|
||||
if ref:
|
||||
refs.add(ref)
|
||||
for e in employees:
|
||||
refs.update(e.get("prompt_refs") or [])
|
||||
|
||||
contents: dict[str, str] = {}
|
||||
for ref in sorted(refs):
|
||||
path = self.opc_home / ref
|
||||
if path.is_file():
|
||||
try:
|
||||
contents[path.name] = path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
logger.debug("Failed to read prompt %s", path)
|
||||
return contents
|
||||
|
||||
def _generate_readme(
|
||||
self,
|
||||
manifest: OPCPackageManifest,
|
||||
roles: list[dict],
|
||||
work_item_templates: list[dict] | None,
|
||||
) -> str:
|
||||
"""Auto-generate a README.md for the package."""
|
||||
lines = [
|
||||
f"# {manifest.name}",
|
||||
"",
|
||||
manifest.description or "An OPC architecture package.",
|
||||
"",
|
||||
f"- **Version**: {manifest.version}",
|
||||
f"- **Category**: {manifest.category}",
|
||||
f"- **Roles**: {manifest.contents.roles}",
|
||||
f"- **Work Item Templates**: {manifest.contents.work_item_templates}",
|
||||
"",
|
||||
"## Roles",
|
||||
"",
|
||||
]
|
||||
for r in roles:
|
||||
lines.append(f"- **{r.get('name', r.get('id', '?'))}** (`{r.get('id', '?')}`): {r.get('responsibility', '')}")
|
||||
if work_item_templates:
|
||||
lines.extend(["", "## Work Item Templates", ""])
|
||||
for item in work_item_templates:
|
||||
lines.append(f"- **{item.get('title', item.get('id', '?'))}** (`{item.get('id', '?')}`)")
|
||||
lines.extend([
|
||||
"",
|
||||
"---",
|
||||
f"*Exported at {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}*",
|
||||
])
|
||||
return "\n".join(lines) + "\n"
|
||||
@@ -0,0 +1,100 @@
|
||||
"""OPC Market package format — data models for .opcpkg packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pydantic models (persisted in YAML / org_config)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class PackageAuthor(BaseModel):
|
||||
name: str = ""
|
||||
github: str = ""
|
||||
|
||||
|
||||
class PackageContents(BaseModel):
|
||||
roles: int = 0
|
||||
work_item_templates: int = 0
|
||||
gates: int = 0
|
||||
prompts: int = 0
|
||||
skills: int = 0
|
||||
|
||||
|
||||
class OPCPackageManifest(BaseModel):
|
||||
"""manifest.yaml schema for an .opcpkg package."""
|
||||
|
||||
opc_package: str = "1.0"
|
||||
id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
version: str = "1.0.0"
|
||||
author: PackageAuthor = Field(default_factory=PackageAuthor)
|
||||
license: str = "MIT"
|
||||
|
||||
category: str = "general"
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
industry: list[str] = Field(default_factory=list)
|
||||
team_size: str = ""
|
||||
use_cases: list[str] = Field(default_factory=list)
|
||||
|
||||
opc_version: str = ">=0.1.0"
|
||||
contents: PackageContents = Field(default_factory=PackageContents)
|
||||
|
||||
|
||||
class InstalledPackageInfo(BaseModel):
|
||||
"""Tracks a package installed into the current org."""
|
||||
|
||||
package_id: str
|
||||
name: str = ""
|
||||
version: str = "1.0.0"
|
||||
installed_at: str = ""
|
||||
source_path: str = ""
|
||||
role_ids: list[str] = Field(default_factory=list)
|
||||
template_ids: list[str] = Field(default_factory=list)
|
||||
work_item_template_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dataclasses (in-memory only, not persisted)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class OPCPackage:
|
||||
"""A fully parsed .opcpkg package ready for install."""
|
||||
|
||||
manifest: OPCPackageManifest
|
||||
roles: list[dict[str, Any]] = field(default_factory=list)
|
||||
work_item_templates: list[dict[str, Any]] = field(default_factory=list)
|
||||
runtime_policy: dict[str, Any] | None = None
|
||||
talent_templates: list[dict[str, Any]] = field(default_factory=list)
|
||||
employees: list[dict[str, Any]] = field(default_factory=list)
|
||||
prompt_contents: dict[str, str] = field(default_factory=dict)
|
||||
skill_contents: dict[str, str] = field(default_factory=dict)
|
||||
readme: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class SandboxReport:
|
||||
"""Result of security validation."""
|
||||
|
||||
passed: bool = True
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
errors: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConflictReport:
|
||||
"""Result of conflict detection against current org."""
|
||||
|
||||
role_conflicts: list[str] = field(default_factory=list)
|
||||
template_conflicts: list[str] = field(default_factory=list)
|
||||
work_item_template_conflicts: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def has_conflicts(self) -> bool:
|
||||
return bool(self.role_conflicts or self.template_conflicts or self.work_item_template_conflicts)
|
||||
@@ -0,0 +1,342 @@
|
||||
"""Load, install, and uninstall OPC Market packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import yaml
|
||||
|
||||
from .package_format import (
|
||||
ConflictReport,
|
||||
InstalledPackageInfo,
|
||||
OPCPackage,
|
||||
OPCPackageManifest,
|
||||
)
|
||||
from .sandbox_checker import SandboxChecker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from opc.core.config import OPCConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_FRONTMATTER_RE = re.compile(r"^---\s*\n.*?\n---\s*\n?", re.DOTALL)
|
||||
|
||||
|
||||
class PackageLoader:
|
||||
"""Loads .opcpkg packages from disk and installs/uninstalls them."""
|
||||
|
||||
def __init__(self, config: OPCConfig, opc_home: Path) -> None:
|
||||
self.config = config
|
||||
self.opc_home = opc_home
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Load
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def load_from_path(self, pkg_path: Path) -> OPCPackage:
|
||||
"""Parse a .opcpkg directory into an OPCPackage."""
|
||||
pkg_path = pkg_path.expanduser().resolve()
|
||||
if not pkg_path.is_dir():
|
||||
raise FileNotFoundError(f"Package directory not found: {pkg_path}")
|
||||
|
||||
# manifest.yaml
|
||||
manifest_path = pkg_path / "manifest.yaml"
|
||||
if not manifest_path.exists():
|
||||
raise FileNotFoundError(f"Missing manifest.yaml in {pkg_path}")
|
||||
with open(manifest_path, encoding="utf-8") as f:
|
||||
manifest_data = yaml.safe_load(f) or {}
|
||||
manifest = OPCPackageManifest.model_validate(manifest_data)
|
||||
|
||||
# org_config.yaml
|
||||
org_path = pkg_path / "org_config.yaml"
|
||||
org_data: dict[str, Any] = {}
|
||||
if org_path.exists():
|
||||
with open(org_path, encoding="utf-8") as f:
|
||||
org_data = yaml.safe_load(f) or {}
|
||||
|
||||
# prompts/
|
||||
prompt_contents: dict[str, str] = {}
|
||||
prompts_dir = pkg_path / "prompts"
|
||||
if prompts_dir.is_dir():
|
||||
for p in sorted(prompts_dir.rglob("*.md")):
|
||||
try:
|
||||
prompt_contents[p.name] = p.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
logger.debug("Failed to read prompt %s", p)
|
||||
|
||||
# README.md
|
||||
readme = ""
|
||||
readme_path = pkg_path / "README.md"
|
||||
if readme_path.exists():
|
||||
readme = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
return OPCPackage(
|
||||
manifest=manifest,
|
||||
roles=org_data.get("roles", []),
|
||||
work_item_templates=org_data.get("work_item_templates", []),
|
||||
runtime_policy=org_data.get("runtime_policy"),
|
||||
talent_templates=org_data.get("talent_templates", []),
|
||||
employees=org_data.get("employees", []),
|
||||
prompt_contents=prompt_contents,
|
||||
readme=readme,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Conflict Detection
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def detect_conflicts(self, package: OPCPackage) -> ConflictReport:
|
||||
"""Check for ID collisions between package and current org."""
|
||||
report = ConflictReport()
|
||||
existing_role_ids = {r.id for r in self.config.org.roles}
|
||||
existing_template_ids = self._current_talent_template_ids()
|
||||
|
||||
for role in package.roles:
|
||||
rid = role.get("id", "")
|
||||
if rid and rid in existing_role_ids:
|
||||
report.role_conflicts.append(rid)
|
||||
|
||||
for tmpl in package.talent_templates:
|
||||
tid = tmpl.get("id", "")
|
||||
if tid and tid in existing_template_ids:
|
||||
report.template_conflicts.append(tid)
|
||||
|
||||
return report
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Install
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def install(
|
||||
self,
|
||||
package: OPCPackage,
|
||||
strategy: str = "namespace",
|
||||
) -> InstalledPackageInfo:
|
||||
"""Install a package into the current org config.
|
||||
|
||||
Args:
|
||||
package: Parsed package to install.
|
||||
strategy: Conflict resolution — "namespace" (prefix IDs) or "overwrite".
|
||||
|
||||
Returns:
|
||||
InstalledPackageInfo to be appended to config.org.installed_packages.
|
||||
Caller must call config.save() to persist.
|
||||
"""
|
||||
pkg_id = package.manifest.id
|
||||
prefix = f"{pkg_id}:" if strategy == "namespace" else ""
|
||||
|
||||
# 1. Write prompt files
|
||||
self._write_prompts(pkg_id, package.prompt_contents)
|
||||
|
||||
# 2. Prepare roles with optional namespace prefix
|
||||
role_ids: list[str] = []
|
||||
for role_data in package.roles:
|
||||
role_data = dict(role_data) # shallow copy
|
||||
original_id = role_data.get("id", "")
|
||||
new_id = f"{prefix}{original_id}" if original_id else original_id
|
||||
role_data["id"] = new_id
|
||||
# Rewrite internal references
|
||||
if prefix:
|
||||
if role_data.get("reports_to") and role_data["reports_to"] != "owner":
|
||||
role_data["reports_to"] = f"{prefix}{role_data['reports_to']}"
|
||||
if role_data.get("can_spawn"):
|
||||
role_data["can_spawn"] = [f"{prefix}{s}" for s in role_data["can_spawn"]]
|
||||
# Rewrite prompt_refs to market directory
|
||||
if role_data.get("prompt_refs"):
|
||||
role_data["prompt_refs"] = [
|
||||
f"prompts/market/{pkg_id}/{Path(ref).name}"
|
||||
for ref in role_data["prompt_refs"]
|
||||
]
|
||||
from opc.core.config import RoleConfig
|
||||
self.config.org.roles.append(RoleConfig.model_validate(role_data))
|
||||
role_ids.append(new_id)
|
||||
|
||||
# 3. Prepare talent templates
|
||||
template_ids: list[str] = []
|
||||
for tmpl_data in package.talent_templates:
|
||||
tmpl_data = dict(tmpl_data)
|
||||
original_id = tmpl_data.get("id", "")
|
||||
new_id = f"{prefix}{original_id}" if original_id else original_id
|
||||
tmpl_data["id"] = new_id
|
||||
if tmpl_data.get("prompt_ref"):
|
||||
tmpl_data["prompt_ref"] = (
|
||||
f"prompts/market/{pkg_id}/{Path(tmpl_data['prompt_ref']).name}"
|
||||
)
|
||||
self._write_talent_template_prompt(pkg_id, tmpl_data, package.prompt_contents)
|
||||
template_ids.append(new_id)
|
||||
|
||||
# 4. Prepare employees
|
||||
for emp_data in package.employees:
|
||||
emp_data = dict(emp_data)
|
||||
if prefix:
|
||||
if emp_data.get("role_id"):
|
||||
emp_data["role_id"] = f"{prefix}{emp_data['role_id']}"
|
||||
if emp_data.get("template_id"):
|
||||
emp_data["template_id"] = f"{prefix}{emp_data['template_id']}"
|
||||
if emp_data.get("employee_id"):
|
||||
emp_data["employee_id"] = f"{prefix}{emp_data['employee_id']}"
|
||||
if emp_data.get("prompt_refs"):
|
||||
emp_data["prompt_refs"] = [
|
||||
f"prompts/market/{pkg_id}/{Path(ref).name}"
|
||||
for ref in emp_data["prompt_refs"]
|
||||
]
|
||||
from opc.core.config import EmployeeConfig
|
||||
self.config.org.employees.append(
|
||||
EmployeeConfig.model_validate(emp_data)
|
||||
)
|
||||
|
||||
# 5. Apply runtime policy if present in package
|
||||
if package.runtime_policy:
|
||||
from opc.core.config import RuntimePolicyConfig
|
||||
try:
|
||||
self.config.org.runtime_policies["custom"] = (
|
||||
RuntimePolicyConfig.model_validate(package.runtime_policy)
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Failed to apply runtime_policy from package %s", pkg_id)
|
||||
|
||||
# 6. Build installation record
|
||||
info = InstalledPackageInfo(
|
||||
package_id=pkg_id,
|
||||
name=package.manifest.name,
|
||||
version=package.manifest.version,
|
||||
installed_at=datetime.now(timezone.utc).isoformat(),
|
||||
source_path=str(package.manifest.id),
|
||||
role_ids=role_ids,
|
||||
template_ids=template_ids,
|
||||
)
|
||||
self.config.org.installed_packages.append(info)
|
||||
|
||||
logger.info(
|
||||
"Installed package %s: %d roles, %d templates",
|
||||
pkg_id, len(role_ids), len(template_ids),
|
||||
)
|
||||
return info
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Uninstall
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def uninstall(self, package_id: str) -> bool:
|
||||
"""Remove an installed package from the org config.
|
||||
|
||||
Caller must call config.save() to persist.
|
||||
"""
|
||||
# Find the installed package record
|
||||
installed = None
|
||||
for pkg in self.config.org.installed_packages:
|
||||
pid = pkg.package_id if isinstance(pkg, InstalledPackageInfo) else pkg.get("package_id", "")
|
||||
if pid == package_id:
|
||||
installed = pkg
|
||||
break
|
||||
if installed is None:
|
||||
logger.warning("Package %s not found in installed_packages", package_id)
|
||||
return False
|
||||
|
||||
if isinstance(installed, InstalledPackageInfo):
|
||||
role_ids = set(installed.role_ids)
|
||||
template_ids = set(installed.template_ids)
|
||||
else:
|
||||
role_ids = set(installed.get("role_ids", []))
|
||||
template_ids = set(installed.get("template_ids", []))
|
||||
|
||||
# Remove roles
|
||||
self.config.org.roles = [
|
||||
r for r in self.config.org.roles if r.id not in role_ids
|
||||
]
|
||||
# Remove materialized package talent templates.
|
||||
for template_id in template_ids:
|
||||
try:
|
||||
self._talent_prompt_path(template_id).unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
# Remove employees belonging to removed roles
|
||||
self.config.org.employees = [
|
||||
e for e in self.config.org.employees
|
||||
if e.role_id not in role_ids
|
||||
]
|
||||
# Remove installed package record
|
||||
self.config.org.installed_packages = [
|
||||
p for p in self.config.org.installed_packages
|
||||
if (p.package_id if isinstance(p, InstalledPackageInfo) else p.get("package_id", "")) != package_id
|
||||
]
|
||||
|
||||
# Remove prompt files
|
||||
prompts_dir = self.opc_home / "prompts" / "market" / package_id
|
||||
if prompts_dir.exists():
|
||||
shutil.rmtree(prompts_dir, ignore_errors=True)
|
||||
|
||||
logger.info("Uninstalled package %s", package_id)
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _write_prompts(self, package_id: str, prompt_contents: dict[str, str]) -> None:
|
||||
"""Write prompt files to {opc_home}/prompts/market/{package_id}/."""
|
||||
if not prompt_contents:
|
||||
return
|
||||
target_dir = self.opc_home / "prompts" / "market" / package_id
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
for filename, content in prompt_contents.items():
|
||||
with open(target_dir / filename, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
def _current_talent_template_ids(self) -> set[str]:
|
||||
try:
|
||||
from opc.layer2_organization.talent_market import TalentMarket
|
||||
|
||||
return {template.id for template in TalentMarket(self.opc_home, self.config).list_available_templates()}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
def _write_talent_template_prompt(
|
||||
self,
|
||||
package_id: str,
|
||||
template_data: dict[str, Any],
|
||||
prompt_contents: dict[str, str],
|
||||
) -> None:
|
||||
template_id = str(template_data.get("id") or "").strip()
|
||||
if not template_id:
|
||||
return
|
||||
prompt_ref = str(template_data.get("prompt_ref") or "").strip()
|
||||
source_name = Path(prompt_ref).name if prompt_ref else ""
|
||||
content = prompt_contents.get(source_name, "") if source_name else ""
|
||||
body = _FRONTMATTER_RE.sub("", content, count=1).strip()
|
||||
name = str(template_data.get("name") or template_id).strip() or template_id
|
||||
description = str(template_data.get("description") or "").strip()
|
||||
if not body:
|
||||
body = f"# {name}\n"
|
||||
if description:
|
||||
body += f"\n{description}\n"
|
||||
frontmatter = {
|
||||
"id": template_id,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"category": str(template_data.get("category") or "general").strip() or "general",
|
||||
"source_package": package_id,
|
||||
}
|
||||
for key in ("domains", "tags", "preferred_external_agent"):
|
||||
value = template_data.get(key)
|
||||
if value not in (None, "", [], {}):
|
||||
frontmatter[key] = value
|
||||
path = self._talent_prompt_path(template_id)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
"---\n"
|
||||
+ yaml.dump(frontmatter, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
+ "---\n\n"
|
||||
+ body.rstrip()
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def _talent_prompt_path(self, template_id: str) -> Path:
|
||||
filename = re.sub(r"[^A-Za-z0-9._:-]+", "-", str(template_id or "").strip()).strip("-") or "template"
|
||||
return self.opc_home / "prompts" / "talent" / f"{filename}.md"
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Security validation for OPC Market packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .package_format import SandboxReport
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .package_format import OPCPackage
|
||||
|
||||
# Tools that could execute arbitrary code or access the filesystem
|
||||
DANGEROUS_TOOLS = frozenset({
|
||||
"shell_exec", "bash", "terminal", "subprocess",
|
||||
"file_write", "file_delete", "file_move",
|
||||
"eval", "exec", "os_command",
|
||||
})
|
||||
|
||||
# Patterns that suggest prompt injection attempts
|
||||
SUSPICIOUS_PROMPT_PATTERNS = [
|
||||
re.compile(r"ignore\s+(all\s+)?previous\s+instructions", re.IGNORECASE),
|
||||
re.compile(r"ignore\s+(all\s+)?above", re.IGNORECASE),
|
||||
re.compile(r"you\s+are\s+now\s+(a|an)\s+", re.IGNORECASE),
|
||||
re.compile(r"system\s*prompt\s*:", re.IGNORECASE),
|
||||
re.compile(r"<\s*system\s*>", re.IGNORECASE),
|
||||
re.compile(r"jailbreak", re.IGNORECASE),
|
||||
re.compile(r"base64\s*decode", re.IGNORECASE),
|
||||
]
|
||||
|
||||
|
||||
class SandboxChecker:
|
||||
"""Validates an OPC package for security concerns before installation."""
|
||||
|
||||
def validate(self, package: OPCPackage) -> SandboxReport:
|
||||
report = SandboxReport()
|
||||
self._check_tools(package, report)
|
||||
self._check_prompts(package, report)
|
||||
self._check_manifest(package, report)
|
||||
report.passed = len(report.errors) == 0
|
||||
return report
|
||||
|
||||
def _check_tools(self, package: OPCPackage, report: SandboxReport) -> None:
|
||||
for role in package.roles:
|
||||
tools = role.get("tools") or []
|
||||
role_id = role.get("id", "unknown")
|
||||
for tool in tools:
|
||||
if tool.lower() in DANGEROUS_TOOLS:
|
||||
report.errors.append(
|
||||
f"Role '{role_id}' uses dangerous tool: {tool}"
|
||||
)
|
||||
|
||||
def _check_prompts(self, package: OPCPackage, report: SandboxReport) -> None:
|
||||
for filename, content in package.prompt_contents.items():
|
||||
for pattern in SUSPICIOUS_PROMPT_PATTERNS:
|
||||
match = pattern.search(content)
|
||||
if match:
|
||||
report.warnings.append(
|
||||
f"Prompt '{filename}' contains suspicious pattern: '{match.group()}'"
|
||||
)
|
||||
|
||||
def _check_manifest(self, package: OPCPackage, report: SandboxReport) -> None:
|
||||
m = package.manifest
|
||||
if not m.id:
|
||||
report.errors.append("Package manifest missing 'id'")
|
||||
if not m.name:
|
||||
report.errors.append("Package manifest missing 'name'")
|
||||
if m.id and not re.match(r"^[a-z0-9][a-z0-9_-]*$", m.id):
|
||||
report.warnings.append(
|
||||
f"Package id '{m.id}' should be lowercase alphanumeric with hyphens/underscores"
|
||||
)
|
||||
@@ -0,0 +1,246 @@
|
||||
"""Built-in talent templates for OPC Market.
|
||||
|
||||
These ensure the marketplace is never empty, even before a user imports
|
||||
any external talent repository. Each template maps to roles in the
|
||||
architecture blueprints exposed by ``architecture_registry.py``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
BUILTIN_TALENT_TEMPLATES: list[dict[str, Any]] = [
|
||||
# ── Management ─────────────────────────────────────────────────
|
||||
{
|
||||
"id": "ceo-strategist",
|
||||
"name": "CEO Strategist",
|
||||
"description": "Executive leader who sets product vision, makes strategic decisions, and coordinates cross-functional teams.",
|
||||
"category": "management",
|
||||
"domains": ["strategy", "leadership", "product-vision"],
|
||||
"tags": ["executive", "decision-maker", "coordinator"],
|
||||
"emoji": "\U0001F454",
|
||||
"color": "#2c3e50",
|
||||
"vibe": "Think big, decide fast, deliver value",
|
||||
},
|
||||
{
|
||||
"id": "account-manager",
|
||||
"name": "Account Manager",
|
||||
"description": "Client-facing relationship manager who scopes projects, tracks deliverables, and ensures client satisfaction.",
|
||||
"category": "management",
|
||||
"domains": ["client-relations", "project-scoping", "delivery"],
|
||||
"tags": ["client-work", "communication", "agency"],
|
||||
"emoji": "\U0001F91D",
|
||||
"color": "#e67e22",
|
||||
"vibe": "The client's voice inside the team",
|
||||
},
|
||||
{
|
||||
"id": "product-manager",
|
||||
"name": "Product Manager",
|
||||
"description": "Defines feature specs, writes user stories, prioritizes backlog, and bridges business and engineering.",
|
||||
"category": "product",
|
||||
"domains": ["product-strategy", "user-stories", "prioritization"],
|
||||
"tags": ["roadmap", "specs", "stakeholder"],
|
||||
"emoji": "\U0001F4CA",
|
||||
"color": "#3498db",
|
||||
"vibe": "Ship the right thing, not just any thing",
|
||||
},
|
||||
|
||||
# ── Engineering ────────────────────────────────────────────────
|
||||
{
|
||||
"id": "cto-architect",
|
||||
"name": "CTO / Tech Architect",
|
||||
"description": "Technical leader who designs system architecture, makes technology choices, and mentors engineering teams.",
|
||||
"category": "engineering",
|
||||
"domains": ["architecture", "tech-leadership", "system-design"],
|
||||
"tags": ["technical", "architecture", "leadership"],
|
||||
"emoji": "\U0001F4BB",
|
||||
"color": "#2980b9",
|
||||
"vibe": "Build it right, scale it further",
|
||||
},
|
||||
{
|
||||
"id": "fullstack-engineer",
|
||||
"name": "Full-Stack Engineer",
|
||||
"description": "Versatile developer handling frontend, backend, databases, and deployment. Ships features end-to-end.",
|
||||
"category": "engineering",
|
||||
"domains": ["frontend", "backend", "database", "deployment"],
|
||||
"tags": ["full-stack", "implementation", "coding"],
|
||||
"emoji": "\u26A1",
|
||||
"color": "#f1c40f",
|
||||
"vibe": "Code it, ship it, fix it, repeat",
|
||||
"preferred_external_agent": "claude_code",
|
||||
},
|
||||
{
|
||||
"id": "devops-engineer",
|
||||
"name": "DevOps / Platform Engineer",
|
||||
"description": "Infrastructure specialist managing CI/CD pipelines, cloud architecture, Kubernetes, and deployment automation.",
|
||||
"category": "engineering",
|
||||
"domains": ["infrastructure", "ci-cd", "kubernetes", "cloud"],
|
||||
"tags": ["devops", "automation", "infrastructure"],
|
||||
"emoji": "\u2699\uFE0F",
|
||||
"color": "#27ae60",
|
||||
"vibe": "Automate everything, trust nothing",
|
||||
"preferred_external_agent": "claude_code",
|
||||
},
|
||||
{
|
||||
"id": "security-engineer",
|
||||
"name": "Security Engineer",
|
||||
"description": "Security specialist performing audits, vulnerability scanning, compliance checks, and threat modeling.",
|
||||
"category": "engineering",
|
||||
"domains": ["security", "compliance", "vulnerability", "audit"],
|
||||
"tags": ["security", "compliance", "audit"],
|
||||
"emoji": "\U0001F6E1\uFE0F",
|
||||
"color": "#c0392b",
|
||||
"vibe": "Assume breach, verify everything",
|
||||
},
|
||||
{
|
||||
"id": "sre-lead",
|
||||
"name": "SRE Lead",
|
||||
"description": "Site reliability engineer managing SLOs, incident response, monitoring, and system resilience.",
|
||||
"category": "engineering",
|
||||
"domains": ["reliability", "monitoring", "incident-response", "slo"],
|
||||
"tags": ["sre", "oncall", "observability"],
|
||||
"emoji": "\U0001F5A5\uFE0F",
|
||||
"color": "#16a085",
|
||||
"vibe": "Keep the lights on, measure everything",
|
||||
},
|
||||
|
||||
# ── Design ─────────────────────────────────────────────────────
|
||||
{
|
||||
"id": "ui-ux-designer",
|
||||
"name": "UI/UX Designer",
|
||||
"description": "Creates user interfaces, prototypes, and conducts user research. Bridges user needs and visual design.",
|
||||
"category": "design",
|
||||
"domains": ["ui-design", "ux-research", "prototyping", "figma"],
|
||||
"tags": ["design", "user-experience", "visual"],
|
||||
"emoji": "\U0001F3A8",
|
||||
"color": "#e74c3c",
|
||||
"vibe": "Design for humans, not for screens",
|
||||
},
|
||||
{
|
||||
"id": "creative-director",
|
||||
"name": "Creative Director",
|
||||
"description": "Sets creative vision, ensures brand consistency, and maintains quality standards across all creative output.",
|
||||
"category": "design",
|
||||
"domains": ["creative-direction", "brand", "quality"],
|
||||
"tags": ["creative", "vision", "brand"],
|
||||
"emoji": "\u2728",
|
||||
"color": "#8e44ad",
|
||||
"vibe": "Every pixel tells a story",
|
||||
},
|
||||
|
||||
# ── Testing ────────────────────────────────────────────────────
|
||||
{
|
||||
"id": "qa-engineer",
|
||||
"name": "QA Engineer",
|
||||
"description": "Tests software quality through manual and automated testing, bug tracking, and regression analysis.",
|
||||
"category": "testing",
|
||||
"domains": ["testing", "quality-assurance", "automation", "bugs"],
|
||||
"tags": ["qa", "testing", "bugs"],
|
||||
"emoji": "\U0001F41B",
|
||||
"color": "#d35400",
|
||||
"vibe": "Break it before users do",
|
||||
"preferred_external_agent": "claude_code",
|
||||
},
|
||||
|
||||
# ── Writing ────────────────────────────────────────────────────
|
||||
{
|
||||
"id": "copywriter",
|
||||
"name": "Copywriter",
|
||||
"description": "Crafts compelling copy, content strategy, messaging, and maintains consistent tone of voice.",
|
||||
"category": "writing",
|
||||
"domains": ["copywriting", "content-strategy", "messaging"],
|
||||
"tags": ["writing", "content", "creative"],
|
||||
"emoji": "\u270D\uFE0F",
|
||||
"color": "#1abc9c",
|
||||
"vibe": "Words that move people to action",
|
||||
},
|
||||
{
|
||||
"id": "editor-in-chief",
|
||||
"name": "Editor-in-Chief",
|
||||
"description": "Manages editorial strategy, content calendar, quality standards, and publishing cadence.",
|
||||
"category": "writing",
|
||||
"domains": ["editorial", "content-calendar", "publishing"],
|
||||
"tags": ["editorial", "publishing", "quality"],
|
||||
"emoji": "\U0001F4F0",
|
||||
"color": "#34495e",
|
||||
"vibe": "Every word earns its place",
|
||||
},
|
||||
|
||||
# ── Research ───────────────────────────────────────────────────
|
||||
{
|
||||
"id": "research-scientist",
|
||||
"name": "Research Scientist",
|
||||
"description": "Designs experiments, analyzes data, writes papers, and pushes the boundaries of knowledge.",
|
||||
"category": "research",
|
||||
"domains": ["experiment-design", "data-analysis", "paper-writing"],
|
||||
"tags": ["research", "academic", "analysis"],
|
||||
"emoji": "\U0001F52C",
|
||||
"color": "#9b59b6",
|
||||
"vibe": "Question everything, prove it twice",
|
||||
},
|
||||
{
|
||||
"id": "data-engineer",
|
||||
"name": "Data Engineer",
|
||||
"description": "Builds data pipelines, manages infrastructure, and ensures data quality and reproducibility.",
|
||||
"category": "data",
|
||||
"domains": ["data-pipelines", "etl", "databases", "reproducibility"],
|
||||
"tags": ["data", "pipelines", "infrastructure"],
|
||||
"emoji": "\U0001F5C4\uFE0F",
|
||||
"color": "#2c3e50",
|
||||
"vibe": "Good data in, good decisions out",
|
||||
"preferred_external_agent": "claude_code",
|
||||
},
|
||||
{
|
||||
"id": "principal-investigator",
|
||||
"name": "Principal Investigator",
|
||||
"description": "Leads research direction, formulates hypotheses, oversees publications, and mentors researchers.",
|
||||
"category": "research",
|
||||
"domains": ["research-direction", "hypothesis", "publication"],
|
||||
"tags": ["pi", "academic", "leadership"],
|
||||
"emoji": "\U0001F393",
|
||||
"color": "#7f8c8d",
|
||||
"vibe": "See what others miss, ask what others won't",
|
||||
},
|
||||
|
||||
# ── Marketing ──────────────────────────────────────────────────
|
||||
{
|
||||
"id": "seo-specialist",
|
||||
"name": "SEO Specialist",
|
||||
"description": "Performs keyword research, optimizes content for search engines, and tracks analytics metrics.",
|
||||
"category": "marketing",
|
||||
"domains": ["seo", "keyword-research", "analytics"],
|
||||
"tags": ["seo", "marketing", "analytics"],
|
||||
"emoji": "\U0001F50D",
|
||||
"color": "#e67e22",
|
||||
"vibe": "Be found before being searched",
|
||||
},
|
||||
{
|
||||
"id": "social-media-manager",
|
||||
"name": "Social Media Manager",
|
||||
"description": "Manages social distribution, community engagement, cross-promotion, and audience growth.",
|
||||
"category": "marketing",
|
||||
"domains": ["social-media", "engagement", "distribution"],
|
||||
"tags": ["social", "marketing", "community"],
|
||||
"emoji": "\U0001F4E2",
|
||||
"color": "#3498db",
|
||||
"vibe": "Turn followers into advocates",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_all_talent_presets() -> list[dict[str, Any]]:
|
||||
"""Return all built-in talent templates."""
|
||||
return BUILTIN_TALENT_TEMPLATES
|
||||
|
||||
|
||||
def get_talent_preset(template_id: str) -> dict[str, Any] | None:
|
||||
"""Return a single built-in talent template by ID."""
|
||||
for t in BUILTIN_TALENT_TEMPLATES:
|
||||
if t["id"] == template_id:
|
||||
return t
|
||||
return None
|
||||
|
||||
|
||||
def get_talent_categories() -> list[str]:
|
||||
"""Return unique categories across all built-in talent templates."""
|
||||
return sorted({t["category"] for t in BUILTIN_TALENT_TEMPLATES})
|
||||
Reference in New Issue
Block a user