tradingcodex 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apps/__init__.py +1 -0
- apps/audit/__init__.py +1 -0
- apps/audit/admin.py +6 -0
- apps/audit/apps.py +8 -0
- apps/audit/migrations/0001_initial.py +35 -0
- apps/audit/migrations/__init__.py +0 -0
- apps/audit/models.py +22 -0
- apps/harness/__init__.py +1 -0
- apps/harness/admin.py +6 -0
- apps/harness/apps.py +8 -0
- apps/harness/migrations/0001_initial.py +35 -0
- apps/harness/migrations/__init__.py +0 -0
- apps/harness/models.py +22 -0
- apps/harness/templatetags/__init__.py +1 -0
- apps/integrations/__init__.py +1 -0
- apps/integrations/admin.py +6 -0
- apps/integrations/apps.py +8 -0
- apps/integrations/migrations/0001_initial.py +29 -0
- apps/integrations/migrations/__init__.py +0 -0
- apps/integrations/models.py +16 -0
- apps/integrations/services.py +31 -0
- apps/mcp/__init__.py +1 -0
- apps/mcp/admin.py +20 -0
- apps/mcp/apps.py +8 -0
- apps/mcp/migrations/0001_initial.py +168 -0
- apps/mcp/migrations/__init__.py +0 -0
- apps/mcp/models.py +154 -0
- apps/mcp/services.py +327 -0
- apps/orders/__init__.py +1 -0
- apps/orders/admin.py +6 -0
- apps/orders/apps.py +8 -0
- apps/orders/migrations/0001_initial.py +79 -0
- apps/orders/migrations/__init__.py +0 -0
- apps/orders/models.py +66 -0
- apps/orders/services.py +107 -0
- apps/policy/__init__.py +1 -0
- apps/policy/admin.py +6 -0
- apps/policy/apps.py +8 -0
- apps/policy/migrations/0001_initial.py +75 -0
- apps/policy/migrations/__init__.py +0 -0
- apps/policy/models.py +61 -0
- apps/policy/services.py +110 -0
- apps/portfolio/__init__.py +1 -0
- apps/portfolio/admin.py +6 -0
- apps/portfolio/apps.py +8 -0
- apps/portfolio/migrations/0001_initial.py +67 -0
- apps/portfolio/migrations/__init__.py +0 -0
- apps/portfolio/models.py +53 -0
- apps/research/__init__.py +1 -0
- apps/research/admin.py +1 -0
- apps/research/apps.py +8 -0
- apps/research/migrations/__init__.py +0 -0
- apps/research/models.py +1 -0
- apps/workflows/__init__.py +1 -0
- apps/workflows/admin.py +6 -0
- apps/workflows/apps.py +8 -0
- apps/workflows/migrations/0001_initial.py +51 -0
- apps/workflows/migrations/__init__.py +0 -0
- apps/workflows/models.py +44 -0
- tradingcodex-0.1.0.dist-info/METADATA +337 -0
- tradingcodex-0.1.0.dist-info/RECORD +254 -0
- tradingcodex-0.1.0.dist-info/WHEEL +5 -0
- tradingcodex-0.1.0.dist-info/entry_points.txt +2 -0
- tradingcodex-0.1.0.dist-info/licenses/LICENSE +202 -0
- tradingcodex-0.1.0.dist-info/licenses/NOTICE +24 -0
- tradingcodex-0.1.0.dist-info/top_level.txt +4 -0
- tradingcodex_cli/__init__.py +1 -0
- tradingcodex_cli/__main__.py +124 -0
- tradingcodex_cli/commands/__init__.py +2 -0
- tradingcodex_cli/commands/bootstrap.py +157 -0
- tradingcodex_cli/commands/db.py +36 -0
- tradingcodex_cli/commands/doctor.py +230 -0
- tradingcodex_cli/commands/mcp.py +186 -0
- tradingcodex_cli/commands/orders.py +89 -0
- tradingcodex_cli/commands/policy.py +21 -0
- tradingcodex_cli/commands/profile.py +110 -0
- tradingcodex_cli/commands/research.py +76 -0
- tradingcodex_cli/commands/skills.py +93 -0
- tradingcodex_cli/commands/strategies.py +67 -0
- tradingcodex_cli/commands/subagents.py +106 -0
- tradingcodex_cli/commands/utils.py +134 -0
- tradingcodex_cli/commands/workspaces.py +53 -0
- tradingcodex_cli/generator.py +234 -0
- tradingcodex_cli/mcp_stdio.py +26 -0
- tradingcodex_cli/service_autostart.py +127 -0
- tradingcodex_service/__init__.py +5 -0
- tradingcodex_service/admin.py +1 -0
- tradingcodex_service/api.py +486 -0
- tradingcodex_service/application/__init__.py +2 -0
- tradingcodex_service/application/agents.py +1470 -0
- tradingcodex_service/application/audit.py +71 -0
- tradingcodex_service/application/common.py +88 -0
- tradingcodex_service/application/components.py +299 -0
- tradingcodex_service/application/harness.py +747 -0
- tradingcodex_service/application/markdown_preview.py +179 -0
- tradingcodex_service/application/orders.py +404 -0
- tradingcodex_service/application/policy.py +150 -0
- tradingcodex_service/application/portfolio.py +166 -0
- tradingcodex_service/application/research.py +356 -0
- tradingcodex_service/application/runtime.py +321 -0
- tradingcodex_service/asgi.py +6 -0
- tradingcodex_service/mcp_http.py +59 -0
- tradingcodex_service/mcp_runtime.py +565 -0
- tradingcodex_service/settings.py +91 -0
- tradingcodex_service/templates/web/activity.html +24 -0
- tradingcodex_service/templates/web/agent_skills.html +111 -0
- tradingcodex_service/templates/web/agents.html +121 -0
- tradingcodex_service/templates/web/base.html +150 -0
- tradingcodex_service/templates/web/dashboard.html +109 -0
- tradingcodex_service/templates/web/fragments/role_inspector.html +81 -0
- tradingcodex_service/templates/web/fragments/starter_prompt.html +24 -0
- tradingcodex_service/templates/web/fragments/topology_canvas.html +85 -0
- tradingcodex_service/templates/web/harness.html +87 -0
- tradingcodex_service/templates/web/mcp_router.html +250 -0
- tradingcodex_service/templates/web/orders.html +68 -0
- tradingcodex_service/templates/web/policy.html +81 -0
- tradingcodex_service/templates/web/portfolio.html +52 -0
- tradingcodex_service/templates/web/research.html +73 -0
- tradingcodex_service/templates/web/starter_prompt.html +40 -0
- tradingcodex_service/templates/web/strategies.html +74 -0
- tradingcodex_service/urls.py +42 -0
- tradingcodex_service/version.py +1 -0
- tradingcodex_service/web.py +885 -0
- tradingcodex_service/wsgi.py +6 -0
- workspace_templates/__init__.py +1 -0
- workspace_templates/modules/audit/files/.tradingcodex/audit/README.md +5 -0
- workspace_templates/modules/audit/files/trading/audit/.gitkeep +1 -0
- workspace_templates/modules/audit/module.json +16 -0
- workspace_templates/modules/codex-base/files/.codex/config.toml +272 -0
- workspace_templates/modules/codex-base/files/.codex/hooks/tradingcodex_hook.py +173 -0
- workspace_templates/modules/codex-base/files/.codex/hooks.json +105 -0
- workspace_templates/modules/codex-base/files/.codex/prompts/base_instructions/head-manager.md +129 -0
- workspace_templates/modules/codex-base/files/.codex/rules/tradingcodex.rules +50 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/capabilities.yaml +56 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/cli.py +16 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/config.yaml +53 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/policy-bindings.yaml +16 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/principals.yaml +17 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/roles.yaml +27 -0
- workspace_templates/modules/codex-base/files/AGENTS.md +56 -0
- workspace_templates/modules/codex-base/files/pyproject.toml +13 -0
- workspace_templates/modules/codex-base/files/tcx +35 -0
- workspace_templates/modules/codex-base/module.json +17 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/access-policies.yaml +39 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/restricted-list.yaml +6 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/approval_receipt.schema.json +14 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/audit_event.schema.json +11 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/evidence_pack.schema.json +16 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/execution_result.schema.json +12 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/fundamental_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/news_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/order_intent.schema.json +30 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/portfolio_review.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/postmortem_report.schema.json +14 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/risk_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/technical_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/thesis.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/valuation.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/scripts/validate-order-intent.py +15 -0
- workspace_templates/modules/enforcement-guardrails/module.json +19 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/execution-operator.toml +71 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/fundamental-analyst.toml +62 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/instrument-analyst.toml +64 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/macro-analyst.toml +64 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/news-analyst.toml +63 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/portfolio-manager.toml +62 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/risk-manager.toml +65 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/technical-analyst.toml +63 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/valuation-analyst.toml +59 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/head-manager.yaml +66 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/skill-change-proposals/.gitkeep +1 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/subagent-registry.yaml +56 -0
- workspace_templates/modules/fixed-subagents/module.json +23 -0
- workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/guardrails.md +17 -0
- workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/task-quality-checklist.md +37 -0
- workspace_templates/modules/guidance-guardrails/module.json +18 -0
- workspace_templates/modules/information-barriers/files/.tradingcodex/policies/information-barriers.yaml +211 -0
- workspace_templates/modules/information-barriers/files/.tradingcodex/secrets.md +9 -0
- workspace_templates/modules/information-barriers/files/trading/approvals/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/market-data/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/approved/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/draft/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/executed/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/rejected/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/portfolio/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/fundamental/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/instrument/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/macro/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/news/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/policy/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/portfolio/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/postmortem/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/risk/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/technical/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/valuation/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/research/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/module.json +19 -0
- workspace_templates/modules/paper-trading/files/.tradingcodex/mcp/adapters/paper-trading.py +4 -0
- workspace_templates/modules/paper-trading/module.json +16 -0
- workspace_templates/modules/postmortem/files/.tradingcodex/workflows/postmortem.yaml +12 -0
- workspace_templates/modules/postmortem/module.json +16 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/SKILL.md +106 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/SKILL.md +101 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/SKILL.md +140 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/SKILL.md +140 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/SKILL.md +31 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/SKILL.md +138 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/SKILL.md +109 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/SKILL.md +54 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/SKILL.md +35 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/SKILL.md +40 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/SKILL.md +40 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/SKILL.md +44 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/SKILL.md +38 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/SKILL.md +45 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/SKILL.md +66 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/SKILL.md +47 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/module.json +37 -0
- workspace_templates/modules/stub-execution/files/.tradingcodex/mcp/adapters/stub-execution.py +4 -0
- workspace_templates/modules/stub-execution/module.json +15 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/adapters/live-adapter.contract.md +25 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/enforcer/README.md +5 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/gateway/README.md +8 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/server.py +27 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/smoke-call.py +18 -0
- workspace_templates/modules/tradingcodex-mcp/module.json +24 -0
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from tradingcodex_service.application.common import _safe_read, now_iso, read_json, sanitize_id, stable_hash, write_json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SkillSpec:
|
|
17
|
+
id: str
|
|
18
|
+
label: str
|
|
19
|
+
owner_roles: tuple[str, ...]
|
|
20
|
+
risk_tags: tuple[str, ...] = ()
|
|
21
|
+
user_visible: bool = False
|
|
22
|
+
scope: str = "mainagent"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class AgentSpec:
|
|
27
|
+
role: str
|
|
28
|
+
label: str
|
|
29
|
+
group: str
|
|
30
|
+
builtin_skills: tuple[str, ...]
|
|
31
|
+
permission_profile: str
|
|
32
|
+
mcp_allowlist: tuple[str, ...] = ()
|
|
33
|
+
forbidden_skill_tags: tuple[str, ...] = ()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
RESEARCH_ROLES = (
|
|
37
|
+
"fundamental-analyst",
|
|
38
|
+
"technical-analyst",
|
|
39
|
+
"news-analyst",
|
|
40
|
+
"macro-analyst",
|
|
41
|
+
"instrument-analyst",
|
|
42
|
+
"valuation-analyst",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
HEAD_MANAGER_SKILLS = (
|
|
46
|
+
"orchestrate-workflow",
|
|
47
|
+
"investment-workflow-map",
|
|
48
|
+
"scenario-quality-gates",
|
|
49
|
+
"manage-subagents",
|
|
50
|
+
"manage-optional-skills",
|
|
51
|
+
"strategy-creator",
|
|
52
|
+
"synthesize-decision",
|
|
53
|
+
"postmortem",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
AGENT_SPECS: dict[str, AgentSpec] = {
|
|
57
|
+
"head-manager": AgentSpec(
|
|
58
|
+
role="head-manager",
|
|
59
|
+
label="Head Manager",
|
|
60
|
+
group="coordination",
|
|
61
|
+
builtin_skills=HEAD_MANAGER_SKILLS,
|
|
62
|
+
permission_profile="tradingcodex",
|
|
63
|
+
mcp_allowlist=(
|
|
64
|
+
"get_tradingcodex_status",
|
|
65
|
+
"simulate_policy",
|
|
66
|
+
"get_order_status",
|
|
67
|
+
"get_positions",
|
|
68
|
+
"get_portfolio_snapshot",
|
|
69
|
+
"list_workflow_artifacts",
|
|
70
|
+
"create_research_artifact",
|
|
71
|
+
"get_research_artifact",
|
|
72
|
+
"list_research_artifacts",
|
|
73
|
+
"search_research_artifacts",
|
|
74
|
+
"append_research_artifact_version",
|
|
75
|
+
"export_research_artifact_md",
|
|
76
|
+
"record_source_snapshot",
|
|
77
|
+
"record_audit_event",
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
"fundamental-analyst": AgentSpec(
|
|
81
|
+
role="fundamental-analyst",
|
|
82
|
+
label="Fundamental Analyst",
|
|
83
|
+
group="research",
|
|
84
|
+
builtin_skills=("external-data-source-gate", "collect-evidence", "fundamental-analysis"),
|
|
85
|
+
permission_profile="tradingcodex-fundamental",
|
|
86
|
+
mcp_allowlist=(
|
|
87
|
+
"list_workflow_artifacts",
|
|
88
|
+
"create_research_artifact",
|
|
89
|
+
"get_research_artifact",
|
|
90
|
+
"list_research_artifacts",
|
|
91
|
+
"search_research_artifacts",
|
|
92
|
+
"append_research_artifact_version",
|
|
93
|
+
"export_research_artifact_md",
|
|
94
|
+
"record_source_snapshot",
|
|
95
|
+
"record_audit_event",
|
|
96
|
+
),
|
|
97
|
+
forbidden_skill_tags=("approval", "execution", "order", "secret"),
|
|
98
|
+
),
|
|
99
|
+
"technical-analyst": AgentSpec(
|
|
100
|
+
role="technical-analyst",
|
|
101
|
+
label="Technical Analyst",
|
|
102
|
+
group="research",
|
|
103
|
+
builtin_skills=("external-data-source-gate", "collect-evidence", "technical-analysis"),
|
|
104
|
+
permission_profile="tradingcodex-technical",
|
|
105
|
+
mcp_allowlist=(
|
|
106
|
+
"list_workflow_artifacts",
|
|
107
|
+
"create_research_artifact",
|
|
108
|
+
"get_research_artifact",
|
|
109
|
+
"list_research_artifacts",
|
|
110
|
+
"search_research_artifacts",
|
|
111
|
+
"append_research_artifact_version",
|
|
112
|
+
"export_research_artifact_md",
|
|
113
|
+
"record_source_snapshot",
|
|
114
|
+
"record_audit_event",
|
|
115
|
+
),
|
|
116
|
+
forbidden_skill_tags=("approval", "execution", "order", "secret"),
|
|
117
|
+
),
|
|
118
|
+
"news-analyst": AgentSpec(
|
|
119
|
+
role="news-analyst",
|
|
120
|
+
label="News Analyst",
|
|
121
|
+
group="research",
|
|
122
|
+
builtin_skills=("external-data-source-gate", "collect-evidence", "news-analysis"),
|
|
123
|
+
permission_profile="tradingcodex-news",
|
|
124
|
+
mcp_allowlist=(
|
|
125
|
+
"list_workflow_artifacts",
|
|
126
|
+
"create_research_artifact",
|
|
127
|
+
"get_research_artifact",
|
|
128
|
+
"list_research_artifacts",
|
|
129
|
+
"search_research_artifacts",
|
|
130
|
+
"append_research_artifact_version",
|
|
131
|
+
"export_research_artifact_md",
|
|
132
|
+
"record_source_snapshot",
|
|
133
|
+
"record_audit_event",
|
|
134
|
+
),
|
|
135
|
+
forbidden_skill_tags=("approval", "execution", "order", "secret"),
|
|
136
|
+
),
|
|
137
|
+
"macro-analyst": AgentSpec(
|
|
138
|
+
role="macro-analyst",
|
|
139
|
+
label="Macro Analyst",
|
|
140
|
+
group="research",
|
|
141
|
+
builtin_skills=("external-data-source-gate", "collect-evidence", "macro-analysis"),
|
|
142
|
+
permission_profile="tradingcodex-macro",
|
|
143
|
+
mcp_allowlist=(
|
|
144
|
+
"list_workflow_artifacts",
|
|
145
|
+
"create_research_artifact",
|
|
146
|
+
"get_research_artifact",
|
|
147
|
+
"list_research_artifacts",
|
|
148
|
+
"search_research_artifacts",
|
|
149
|
+
"append_research_artifact_version",
|
|
150
|
+
"export_research_artifact_md",
|
|
151
|
+
"record_source_snapshot",
|
|
152
|
+
"record_audit_event",
|
|
153
|
+
),
|
|
154
|
+
forbidden_skill_tags=("approval", "execution", "order", "secret"),
|
|
155
|
+
),
|
|
156
|
+
"instrument-analyst": AgentSpec(
|
|
157
|
+
role="instrument-analyst",
|
|
158
|
+
label="Instrument Analyst",
|
|
159
|
+
group="research",
|
|
160
|
+
builtin_skills=("external-data-source-gate", "collect-evidence", "instrument-analysis"),
|
|
161
|
+
permission_profile="tradingcodex-instrument",
|
|
162
|
+
mcp_allowlist=(
|
|
163
|
+
"list_workflow_artifacts",
|
|
164
|
+
"create_research_artifact",
|
|
165
|
+
"get_research_artifact",
|
|
166
|
+
"list_research_artifacts",
|
|
167
|
+
"search_research_artifacts",
|
|
168
|
+
"append_research_artifact_version",
|
|
169
|
+
"export_research_artifact_md",
|
|
170
|
+
"record_source_snapshot",
|
|
171
|
+
"record_audit_event",
|
|
172
|
+
),
|
|
173
|
+
forbidden_skill_tags=("approval", "execution", "order", "secret"),
|
|
174
|
+
),
|
|
175
|
+
"valuation-analyst": AgentSpec(
|
|
176
|
+
role="valuation-analyst",
|
|
177
|
+
label="Valuation Analyst",
|
|
178
|
+
group="research",
|
|
179
|
+
builtin_skills=("external-data-source-gate", "valuation-review"),
|
|
180
|
+
permission_profile="tradingcodex-valuation",
|
|
181
|
+
mcp_allowlist=(
|
|
182
|
+
"list_workflow_artifacts",
|
|
183
|
+
"create_research_artifact",
|
|
184
|
+
"get_research_artifact",
|
|
185
|
+
"list_research_artifacts",
|
|
186
|
+
"search_research_artifacts",
|
|
187
|
+
"append_research_artifact_version",
|
|
188
|
+
"export_research_artifact_md",
|
|
189
|
+
"record_source_snapshot",
|
|
190
|
+
"record_audit_event",
|
|
191
|
+
),
|
|
192
|
+
forbidden_skill_tags=("approval", "execution", "order", "secret"),
|
|
193
|
+
),
|
|
194
|
+
"portfolio-manager": AgentSpec(
|
|
195
|
+
role="portfolio-manager",
|
|
196
|
+
label="Portfolio Manager",
|
|
197
|
+
group="portfolio",
|
|
198
|
+
builtin_skills=("portfolio-review", "create-order-intent"),
|
|
199
|
+
permission_profile="tradingcodex-portfolio",
|
|
200
|
+
mcp_allowlist=("list_workflow_artifacts", "get_positions", "get_portfolio_snapshot", "record_audit_event"),
|
|
201
|
+
forbidden_skill_tags=("execution", "secret"),
|
|
202
|
+
),
|
|
203
|
+
"risk-manager": AgentSpec(
|
|
204
|
+
role="risk-manager",
|
|
205
|
+
label="Risk Manager",
|
|
206
|
+
group="risk",
|
|
207
|
+
builtin_skills=("review-risk", "policy-review", "approve-order"),
|
|
208
|
+
permission_profile="tradingcodex-risk",
|
|
209
|
+
mcp_allowlist=(
|
|
210
|
+
"simulate_policy",
|
|
211
|
+
"validate_order_intent",
|
|
212
|
+
"validate_approval_receipt",
|
|
213
|
+
"create_approval_receipt",
|
|
214
|
+
"list_workflow_artifacts",
|
|
215
|
+
"record_audit_event",
|
|
216
|
+
),
|
|
217
|
+
forbidden_skill_tags=("execution", "secret"),
|
|
218
|
+
),
|
|
219
|
+
"execution-operator": AgentSpec(
|
|
220
|
+
role="execution-operator",
|
|
221
|
+
label="Execution Operator",
|
|
222
|
+
group="execution",
|
|
223
|
+
builtin_skills=("execute-paper-order",),
|
|
224
|
+
permission_profile="tradingcodex-execution",
|
|
225
|
+
mcp_allowlist=(
|
|
226
|
+
"simulate_policy",
|
|
227
|
+
"validate_order_intent",
|
|
228
|
+
"validate_approval_receipt",
|
|
229
|
+
"submit_approved_order",
|
|
230
|
+
"cancel_approved_order",
|
|
231
|
+
"get_order_status",
|
|
232
|
+
"get_positions",
|
|
233
|
+
"get_portfolio_snapshot",
|
|
234
|
+
"list_workflow_artifacts",
|
|
235
|
+
"record_audit_event",
|
|
236
|
+
),
|
|
237
|
+
forbidden_skill_tags=("approval", "secret"),
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
SKILL_SPECS: dict[str, SkillSpec] = {
|
|
243
|
+
"orchestrate-workflow": SkillSpec("orchestrate-workflow", "Orchestrate Workflow", ("head-manager",), user_visible=True),
|
|
244
|
+
"investment-workflow-map": SkillSpec("investment-workflow-map", "Investment Workflow Map", ("head-manager",)),
|
|
245
|
+
"scenario-quality-gates": SkillSpec("scenario-quality-gates", "Scenario Quality Gates", ("head-manager",)),
|
|
246
|
+
"external-data-source-gate": SkillSpec("external-data-source-gate", "External Data Source Gate", RESEARCH_ROLES, scope="subagent_shared"),
|
|
247
|
+
"manage-subagents": SkillSpec("manage-subagents", "Manage Subagents", ("head-manager",)),
|
|
248
|
+
"manage-optional-skills": SkillSpec("manage-optional-skills", "Manage Optional Skills", ("head-manager",)),
|
|
249
|
+
"strategy-creator": SkillSpec("strategy-creator", "Strategy Creator", ("head-manager",), user_visible=True),
|
|
250
|
+
"synthesize-decision": SkillSpec("synthesize-decision", "Synthesize Decision", ("head-manager",)),
|
|
251
|
+
"postmortem": SkillSpec("postmortem", "Postmortem", ("head-manager",), user_visible=True),
|
|
252
|
+
"collect-evidence": SkillSpec("collect-evidence", "Collect Evidence", RESEARCH_ROLES, scope="subagent_shared"),
|
|
253
|
+
"fundamental-analysis": SkillSpec("fundamental-analysis", "Fundamental Analysis", ("fundamental-analyst",), scope="subagent_role"),
|
|
254
|
+
"technical-analysis": SkillSpec("technical-analysis", "Technical Analysis", ("technical-analyst",), scope="subagent_role"),
|
|
255
|
+
"news-analysis": SkillSpec("news-analysis", "News Analysis", ("news-analyst",), scope="subagent_role"),
|
|
256
|
+
"macro-analysis": SkillSpec("macro-analysis", "Macro Analysis", ("macro-analyst",), scope="subagent_role"),
|
|
257
|
+
"instrument-analysis": SkillSpec("instrument-analysis", "Instrument Analysis", ("instrument-analyst",), scope="subagent_role"),
|
|
258
|
+
"valuation-review": SkillSpec("valuation-review", "Valuation Review", ("valuation-analyst",), scope="subagent_role"),
|
|
259
|
+
"portfolio-review": SkillSpec("portfolio-review", "Portfolio Review", ("portfolio-manager",), scope="subagent_role"),
|
|
260
|
+
"create-order-intent": SkillSpec("create-order-intent", "Create Order Intent", ("portfolio-manager",), risk_tags=("order",), scope="subagent_role"),
|
|
261
|
+
"review-risk": SkillSpec("review-risk", "Review Risk", ("risk-manager",), scope="subagent_role"),
|
|
262
|
+
"policy-review": SkillSpec("policy-review", "Policy Review", ("risk-manager",), risk_tags=("approval",), scope="subagent_role"),
|
|
263
|
+
"approve-order": SkillSpec("approve-order", "Approve Order", ("risk-manager",), risk_tags=("approval", "order"), scope="subagent_role"),
|
|
264
|
+
"execute-paper-order": SkillSpec("execute-paper-order", "Execute Paper Order", ("execution-operator",), risk_tags=("execution", "order"), scope="subagent_role"),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
ROLE_SKILL_MAP: dict[str, list[str]] = {role: list(spec.builtin_skills) for role, spec in AGENT_SPECS.items()}
|
|
269
|
+
USER_VISIBLE_SKILLS = [skill.id for skill in SKILL_SPECS.values() if skill.user_visible]
|
|
270
|
+
EXPECTED_SUBAGENTS = [role for role in AGENT_SPECS if role != "head-manager"]
|
|
271
|
+
EXPECTED_SKILLS = sorted(SKILL_SPECS)
|
|
272
|
+
ROLE_PERMISSION_PROFILES = {role: spec.permission_profile for role, spec in AGENT_SPECS.items() if role != "head-manager"}
|
|
273
|
+
|
|
274
|
+
PROPOSAL_DIR = Path(".tradingcodex/mainagent/skill-change-proposals")
|
|
275
|
+
MAINAGENT_SKILL_DIR = Path(".agents/skills")
|
|
276
|
+
STRATEGY_SKILL_DIR = Path(".tradingcodex/strategies")
|
|
277
|
+
SUBAGENT_SKILL_DIR = Path(".tradingcodex/subagents/skills")
|
|
278
|
+
SUBAGENT_SHARED_SKILL_DIR = SUBAGENT_SKILL_DIR / "shared"
|
|
279
|
+
OPTIONAL_SKILL_STATUS_FILE = Path("agents/tradingcodex.json")
|
|
280
|
+
ADDITIONAL_INSTRUCTION_DIR = Path(".tradingcodex/agent-instructions")
|
|
281
|
+
ADDITIONAL_INSTRUCTION_START = "## BEGIN TradingCodex additional instructions"
|
|
282
|
+
ADDITIONAL_INSTRUCTION_END = "## END TradingCodex additional instructions"
|
|
283
|
+
GENERATED_DIR = Path(".tradingcodex/generated")
|
|
284
|
+
MANIFEST_PATH = GENERATED_DIR / "projection-manifest.json"
|
|
285
|
+
AGENT_INDEX_PATH = GENERATED_DIR / "agent-index.json"
|
|
286
|
+
SKILL_INDEX_PATH = GENERATED_DIR / "skill-index.json"
|
|
287
|
+
STRATEGY_SKILL_PREFIX = "strategy-"
|
|
288
|
+
STRATEGY_ROOT_CONFIG_START = "# BEGIN TradingCodex strategy skills"
|
|
289
|
+
STRATEGY_ROOT_CONFIG_END = "# END TradingCodex strategy skills"
|
|
290
|
+
STRATEGY_REQUIRED_FRONTMATTER = {
|
|
291
|
+
"name",
|
|
292
|
+
"description",
|
|
293
|
+
"type",
|
|
294
|
+
"status",
|
|
295
|
+
"language",
|
|
296
|
+
"managed_by",
|
|
297
|
+
"owner",
|
|
298
|
+
"last_reviewed",
|
|
299
|
+
}
|
|
300
|
+
STRATEGY_REQUIRED_SECTIONS = (
|
|
301
|
+
"## Thesis",
|
|
302
|
+
"## Eligible Universe",
|
|
303
|
+
"## Preferred Setups",
|
|
304
|
+
"## Entry Criteria",
|
|
305
|
+
"## Exit Criteria",
|
|
306
|
+
"## Evidence Requirements",
|
|
307
|
+
"## Decision-Ready Standard",
|
|
308
|
+
"## Sizing Guidance",
|
|
309
|
+
"## Block Conditions",
|
|
310
|
+
"## Portfolio And Risk Handoff",
|
|
311
|
+
"## Change Log",
|
|
312
|
+
)
|
|
313
|
+
OPTIONAL_SKILL_STATUSES = {"draft", "active", "archived"}
|
|
314
|
+
SKILL_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9-]{2,63}$")
|
|
315
|
+
OPTIONAL_SKILL_RISK_PATTERNS = {
|
|
316
|
+
"approval": re.compile(r"\b(approve|approval|approval receipt|receipt)\b", re.I),
|
|
317
|
+
"execution": re.compile(r"\b(execute|execution|submit order|adapter submission|broker)\b", re.I),
|
|
318
|
+
"order": re.compile(r"\b(order|order intent|buy|sell|short|long|trade|trading)\b", re.I),
|
|
319
|
+
"secret": re.compile(r"\b(secret|credential|token|api key|password|\\.env)\b", re.I),
|
|
320
|
+
}
|
|
321
|
+
OPTIONAL_SKILL_LOCKED_SURFACE_PATTERN = re.compile(
|
|
322
|
+
r"\b("
|
|
323
|
+
r"mcp allowlist|permission profile|raw broker|live broker|direct broker|"
|
|
324
|
+
r"bypass|ignore policy|disable guardrail|weaken guardrail|self-approve|"
|
|
325
|
+
r"change policy|change capability|read secret|secret access"
|
|
326
|
+
r")\b",
|
|
327
|
+
re.I,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def registry_summary() -> dict[str, Any]:
|
|
332
|
+
return {
|
|
333
|
+
"source": "tradingcodex_service.application.agents",
|
|
334
|
+
"agents": {role: _agent_spec_payload(spec) for role, spec in AGENT_SPECS.items()},
|
|
335
|
+
"skills": {skill_id: asdict(spec) for skill_id, spec in SKILL_SPECS.items()},
|
|
336
|
+
"expected_subagents": EXPECTED_SUBAGENTS,
|
|
337
|
+
"expected_skills": EXPECTED_SKILLS,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def inspect_agent_configuration(root: Path | str, role: str) -> dict[str, Any]:
|
|
342
|
+
root = Path(root).resolve()
|
|
343
|
+
if role not in AGENT_SPECS:
|
|
344
|
+
raise ValueError(f"Unknown subagent or role: {role}")
|
|
345
|
+
state = build_projection_state(root)
|
|
346
|
+
return state["agents"][role]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def list_optional_role_skills(root: Path | str, role: str | None = None, include_archived: bool = True) -> dict[str, Any]:
|
|
350
|
+
root = Path(root).resolve()
|
|
351
|
+
records = read_optional_skill_records(root, role=role, include_archived=include_archived)
|
|
352
|
+
return {
|
|
353
|
+
"status": "ok",
|
|
354
|
+
"source": "file-native-optional-skills",
|
|
355
|
+
"read_only": True,
|
|
356
|
+
"optional_skills": records,
|
|
357
|
+
"roles": sorted({record["role"] for record in records}),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def list_user_visible_skills(root: Path | str) -> list[str]:
|
|
362
|
+
root = Path(root).resolve()
|
|
363
|
+
if not (root / MAINAGENT_SKILL_DIR).exists():
|
|
364
|
+
return list(USER_VISIBLE_SKILLS)
|
|
365
|
+
installed = _installed_skill_index(root)
|
|
366
|
+
visible = [skill for skill in USER_VISIBLE_SKILLS if installed.get(skill, {}).get("installed")]
|
|
367
|
+
visible.extend(skill["name"] for skill in read_strategy_skill_records(root, active_only=True))
|
|
368
|
+
return list(dict.fromkeys(visible))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def read_strategy_skill_records(root: Path | str, *, active_only: bool = False) -> list[dict[str, Any]]:
|
|
372
|
+
root = Path(root).resolve()
|
|
373
|
+
records: list[dict[str, Any]] = []
|
|
374
|
+
primary_root = root / STRATEGY_SKILL_DIR
|
|
375
|
+
if primary_root.exists():
|
|
376
|
+
for skill_path in sorted(primary_root.glob(f"{STRATEGY_SKILL_PREFIX}*/SKILL.md")):
|
|
377
|
+
record = _strategy_record_payload(root, skill_path)
|
|
378
|
+
if active_only and not record["active"]:
|
|
379
|
+
continue
|
|
380
|
+
records.append(record)
|
|
381
|
+
return records
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def read_optional_skill_records(root: Path | str, role: str | None = None, include_archived: bool = True) -> list[dict[str, Any]]:
|
|
385
|
+
root = Path(root).resolve()
|
|
386
|
+
records: list[dict[str, Any]] = []
|
|
387
|
+
base = root / SUBAGENT_SKILL_DIR
|
|
388
|
+
if base.exists():
|
|
389
|
+
for path in sorted(base.glob("*/*/SKILL.md")):
|
|
390
|
+
scope_name = path.parent.parent.name
|
|
391
|
+
name = path.parent.name
|
|
392
|
+
if name in SKILL_SPECS:
|
|
393
|
+
continue
|
|
394
|
+
metadata_path = path.parent / OPTIONAL_SKILL_STATUS_FILE
|
|
395
|
+
record = read_json(metadata_path, {}) or {}
|
|
396
|
+
roles = _optional_record_roles(record, scope_name)
|
|
397
|
+
for target_role in roles:
|
|
398
|
+
if role and target_role != role:
|
|
399
|
+
continue
|
|
400
|
+
candidate = {
|
|
401
|
+
**record,
|
|
402
|
+
"role": target_role,
|
|
403
|
+
"name": name,
|
|
404
|
+
"scope": "shared" if scope_name == "shared" else "role",
|
|
405
|
+
"source_file": _relative_path(root, path),
|
|
406
|
+
"metadata_file": _relative_path(root, path.parent / "agents" / "openai.yaml"),
|
|
407
|
+
"status_file": _relative_path(root, metadata_path),
|
|
408
|
+
}
|
|
409
|
+
payload = _optional_record_payload(root, candidate)
|
|
410
|
+
if not include_archived and payload.get("status") == "archived":
|
|
411
|
+
continue
|
|
412
|
+
records.append(payload)
|
|
413
|
+
return records
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def read_agent_additional_instructions(root: Path | str, role: str) -> dict[str, Any]:
|
|
417
|
+
root = Path(root).resolve()
|
|
418
|
+
if role not in AGENT_SPECS:
|
|
419
|
+
raise ValueError(f"Unknown subagent or role: {role}")
|
|
420
|
+
path = _additional_instruction_path(root, role)
|
|
421
|
+
body = _safe_read(path)
|
|
422
|
+
return {
|
|
423
|
+
"role": role,
|
|
424
|
+
"body": body,
|
|
425
|
+
"installed": path.exists(),
|
|
426
|
+
"source_file": _relative_path(root, path),
|
|
427
|
+
"source_file_hash": _file_hash(path),
|
|
428
|
+
"line_count": len(body.splitlines()) if body else 0,
|
|
429
|
+
"char_count": len(body),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def write_agent_additional_instructions(
|
|
434
|
+
root: Path | str,
|
|
435
|
+
role: str,
|
|
436
|
+
body: str,
|
|
437
|
+
*,
|
|
438
|
+
actor: str = "local",
|
|
439
|
+
) -> dict[str, Any]:
|
|
440
|
+
root = Path(root).resolve()
|
|
441
|
+
if role not in AGENT_SPECS:
|
|
442
|
+
raise ValueError(f"Unknown subagent or role: {role}")
|
|
443
|
+
path = _additional_instruction_path(root, role)
|
|
444
|
+
cleaned = str(body or "").strip()
|
|
445
|
+
if cleaned:
|
|
446
|
+
_atomic_write_text(path, cleaned + "\n")
|
|
447
|
+
else:
|
|
448
|
+
path.unlink(missing_ok=True)
|
|
449
|
+
project_agent_configuration(root, role=role, applied_by=actor)
|
|
450
|
+
return read_agent_additional_instructions(root, role)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def validate_optional_skill_payload(role: str, name: str, description: str = "", body: str = "") -> dict[str, Any]:
|
|
454
|
+
errors: list[str] = []
|
|
455
|
+
if role not in EXPECTED_SUBAGENTS:
|
|
456
|
+
errors.append(f"optional skills can target fixed subagents only: {role}")
|
|
457
|
+
if name in SKILL_SPECS:
|
|
458
|
+
errors.append(f"core skill cannot be overwritten: {name}")
|
|
459
|
+
if name.startswith(STRATEGY_SKILL_PREFIX):
|
|
460
|
+
errors.append("optional skill name cannot use reserved strategy- prefix")
|
|
461
|
+
if not SKILL_NAME_PATTERN.match(name):
|
|
462
|
+
errors.append("optional skill name must be lowercase hyphen-case, 3-64 characters")
|
|
463
|
+
combined = "\n".join([name, description, body])
|
|
464
|
+
if OPTIONAL_SKILL_LOCKED_SURFACE_PATTERN.search(combined):
|
|
465
|
+
errors.append("optional skills cannot change locked harness surfaces")
|
|
466
|
+
risk_tags = infer_optional_skill_risk_tags(combined)
|
|
467
|
+
agent = AGENT_SPECS.get(role)
|
|
468
|
+
if agent:
|
|
469
|
+
blocked_tags = sorted(set(agent.forbidden_skill_tags).intersection(risk_tags))
|
|
470
|
+
if blocked_tags:
|
|
471
|
+
errors.append(f"{role} cannot receive {name}; blocked risk tags: {', '.join(blocked_tags)}")
|
|
472
|
+
return {"status": "blocked" if errors else "valid", "errors": errors, "risk_tags": risk_tags}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def infer_optional_skill_risk_tags(text: str) -> list[str]:
|
|
476
|
+
return sorted(tag for tag, pattern in OPTIONAL_SKILL_RISK_PATTERNS.items() if pattern.search(text))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def normalize_optional_skill_name(raw: str) -> str:
|
|
480
|
+
return sanitize_id(raw).strip("-").lower()
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def create_or_update_strategy_skill(
|
|
484
|
+
root: Path | str,
|
|
485
|
+
name: str,
|
|
486
|
+
*,
|
|
487
|
+
description: str = "",
|
|
488
|
+
body: str = "",
|
|
489
|
+
language: str = "unknown",
|
|
490
|
+
status: str = "draft",
|
|
491
|
+
actor: str = "local",
|
|
492
|
+
) -> dict[str, Any]:
|
|
493
|
+
root = Path(root).resolve()
|
|
494
|
+
name = normalize_strategy_skill_name(name)
|
|
495
|
+
if status not in {"draft", "active", "archived"}:
|
|
496
|
+
raise ValueError(f"unknown strategy status: {status}")
|
|
497
|
+
if (root / MAINAGENT_SKILL_DIR / name).exists():
|
|
498
|
+
raise ValueError(f"strategy skill must be managed under .tradingcodex/strategies: {name}")
|
|
499
|
+
skill_dir = root / STRATEGY_SKILL_DIR / name
|
|
500
|
+
skill_path = skill_dir / "SKILL.md"
|
|
501
|
+
current_fields = _read_frontmatter_fields(skill_path)
|
|
502
|
+
current_body = _read_markdown_body(skill_path)
|
|
503
|
+
current_name = current_fields.get("name") or name
|
|
504
|
+
if current_name != name:
|
|
505
|
+
raise ValueError(f"strategy skill name must match its directory: {name}")
|
|
506
|
+
description = description or current_fields.get("description") or f"Apply the {name} strategy."
|
|
507
|
+
language = language or current_fields.get("language") or "unknown"
|
|
508
|
+
body = body if body.strip() else current_body or _default_strategy_body(name)
|
|
509
|
+
text = _render_strategy_skill_markdown(name, description, body, language, status)
|
|
510
|
+
_atomic_write_text(skill_path, text)
|
|
511
|
+
_atomic_write_text(skill_dir / "agents" / "openai.yaml", _render_openai_yaml(_strategy_display_name(name, body), description, f"Use ${name} to apply this user-approved strategy."))
|
|
512
|
+
record = _strategy_record_payload(root, skill_path)
|
|
513
|
+
if status == "active" and record["validation_errors"]:
|
|
514
|
+
raise ValueError("; ".join(record["validation_errors"]))
|
|
515
|
+
project_agent_configuration(root, applied_by=actor)
|
|
516
|
+
return _strategy_record_payload(root, skill_path)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def set_strategy_skill_status(root: Path | str, name: str, status: str, *, actor: str = "local") -> dict[str, Any]:
|
|
520
|
+
root = Path(root).resolve()
|
|
521
|
+
name = normalize_strategy_skill_name(name)
|
|
522
|
+
record = get_strategy_skill_record(root, name)
|
|
523
|
+
fields = dict(record.get("frontmatter") or {})
|
|
524
|
+
body = _read_markdown_body(root / str(record["source_file"]))
|
|
525
|
+
updated = create_or_update_strategy_skill(
|
|
526
|
+
root,
|
|
527
|
+
name,
|
|
528
|
+
description=fields.get("description", ""),
|
|
529
|
+
body=body,
|
|
530
|
+
language=fields.get("language", "unknown"),
|
|
531
|
+
status=status,
|
|
532
|
+
actor=actor,
|
|
533
|
+
)
|
|
534
|
+
return updated
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def delete_strategy_skill(root: Path | str, name: str, *, force: bool = False, actor: str = "local") -> dict[str, Any]:
|
|
538
|
+
root = Path(root).resolve()
|
|
539
|
+
name = normalize_strategy_skill_name(name)
|
|
540
|
+
record = get_strategy_skill_record(root, name)
|
|
541
|
+
if record.get("status") == "active" and not force:
|
|
542
|
+
return set_strategy_skill_status(root, name, "archived", actor=actor)
|
|
543
|
+
shutil.rmtree(root / STRATEGY_SKILL_DIR / name, ignore_errors=True)
|
|
544
|
+
project_agent_configuration(root, applied_by=actor)
|
|
545
|
+
return {"name": name, "status": "deleted", "active": False}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def get_strategy_skill_record(root: Path | str, name: str) -> dict[str, Any]:
|
|
549
|
+
name = normalize_strategy_skill_name(name)
|
|
550
|
+
for record in read_strategy_skill_records(root, active_only=False):
|
|
551
|
+
if record["name"] == name:
|
|
552
|
+
return record
|
|
553
|
+
raise ValueError(f"unknown strategy: {name}")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def normalize_strategy_skill_name(raw: str) -> str:
|
|
557
|
+
name = sanitize_id(raw).strip("-").lower()
|
|
558
|
+
if not name.startswith(STRATEGY_SKILL_PREFIX):
|
|
559
|
+
raise ValueError("strategy skill name must start with strategy-")
|
|
560
|
+
if not SKILL_NAME_PATTERN.match(name):
|
|
561
|
+
raise ValueError("strategy skill name must be lowercase hyphen-case, 3-64 characters")
|
|
562
|
+
return name
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def create_or_update_optional_skill(
|
|
566
|
+
root: Path | str,
|
|
567
|
+
role: str,
|
|
568
|
+
name: str,
|
|
569
|
+
*,
|
|
570
|
+
description: str = "",
|
|
571
|
+
body: str = "",
|
|
572
|
+
status: str = "draft",
|
|
573
|
+
actor: str = "local",
|
|
574
|
+
) -> dict[str, Any]:
|
|
575
|
+
root = Path(root).resolve()
|
|
576
|
+
if role not in EXPECTED_SUBAGENTS:
|
|
577
|
+
raise ValueError(f"optional skills can target fixed subagents only: {role}")
|
|
578
|
+
name = normalize_optional_skill_name(name)
|
|
579
|
+
if status not in OPTIONAL_SKILL_STATUSES:
|
|
580
|
+
raise ValueError(f"unknown optional skill status: {status}")
|
|
581
|
+
if name in SKILL_SPECS or (root / MAINAGENT_SKILL_DIR / name).exists():
|
|
582
|
+
raise ValueError(f"core or project-scope skill cannot be overwritten: {name}")
|
|
583
|
+
skill_dir = root / SUBAGENT_SKILL_DIR / role / name
|
|
584
|
+
skill_path = skill_dir / "SKILL.md"
|
|
585
|
+
current_fields = _read_frontmatter_fields(skill_path)
|
|
586
|
+
current_body = _read_markdown_body(skill_path)
|
|
587
|
+
current_name = current_fields.get("name") or name
|
|
588
|
+
if current_name != name:
|
|
589
|
+
raise ValueError(f"optional skill name must match its directory: {name}")
|
|
590
|
+
description = description or current_fields.get("description") or f"Use the {name} optional procedure."
|
|
591
|
+
body = body if body.strip() else current_body or f"# {_skill_display_name(name, '')}\n\nDescribe the optional role-local procedure.\n"
|
|
592
|
+
validation = validate_optional_skill_payload(role, name, description, body)
|
|
593
|
+
if status == "active" and validation["errors"]:
|
|
594
|
+
raise ValueError("; ".join(validation["errors"]))
|
|
595
|
+
_atomic_write_text(skill_path, _render_basic_skill_markdown(name, description, body))
|
|
596
|
+
_atomic_write_text(skill_dir / "agents" / "openai.yaml", _render_openai_yaml(_skill_display_name(name, body), description, f"Use ${name} for the {role} optional procedure."))
|
|
597
|
+
metadata = {
|
|
598
|
+
"role": role,
|
|
599
|
+
"name": name,
|
|
600
|
+
"scope": "role",
|
|
601
|
+
"status": status,
|
|
602
|
+
"updated_by": actor,
|
|
603
|
+
"updated_at": now_iso(),
|
|
604
|
+
}
|
|
605
|
+
status_path = skill_dir / OPTIONAL_SKILL_STATUS_FILE
|
|
606
|
+
existing = read_json(status_path, {}) or {}
|
|
607
|
+
if existing.get("created_at"):
|
|
608
|
+
metadata["created_at"] = existing["created_at"]
|
|
609
|
+
metadata["created_by"] = existing.get("created_by", actor)
|
|
610
|
+
else:
|
|
611
|
+
metadata["created_at"] = metadata["updated_at"]
|
|
612
|
+
metadata["created_by"] = actor
|
|
613
|
+
_atomic_write_json(status_path, metadata)
|
|
614
|
+
project_agent_configuration(root, role=role, applied_by=actor)
|
|
615
|
+
return next(record for record in read_optional_skill_records(root, role=role, include_archived=True) if record["name"] == name)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def set_optional_skill_status(root: Path | str, role: str, name: str, status: str, *, actor: str = "local") -> dict[str, Any]:
|
|
619
|
+
root = Path(root).resolve()
|
|
620
|
+
name = normalize_optional_skill_name(name)
|
|
621
|
+
record = get_optional_skill_record(root, role, name)
|
|
622
|
+
body = _read_markdown_body(root / str(record["source_file"]))
|
|
623
|
+
return create_or_update_optional_skill(
|
|
624
|
+
root,
|
|
625
|
+
role,
|
|
626
|
+
name,
|
|
627
|
+
description=str(record.get("description") or ""),
|
|
628
|
+
body=body,
|
|
629
|
+
status=status,
|
|
630
|
+
actor=actor,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def delete_optional_skill(root: Path | str, role: str, name: str, *, force: bool = False, actor: str = "local") -> dict[str, Any]:
|
|
635
|
+
root = Path(root).resolve()
|
|
636
|
+
name = normalize_optional_skill_name(name)
|
|
637
|
+
record = get_optional_skill_record(root, role, name)
|
|
638
|
+
if record.get("status") == "active" and not force:
|
|
639
|
+
return set_optional_skill_status(root, role, name, "archived", actor=actor)
|
|
640
|
+
source = root / str(record["source_file"])
|
|
641
|
+
shutil.rmtree(source.parent, ignore_errors=True)
|
|
642
|
+
project_agent_configuration(root, role=role, applied_by=actor)
|
|
643
|
+
return {"role": role, "name": name, "status": "deleted"}
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def get_optional_skill_record(root: Path | str, role: str, name: str) -> dict[str, Any]:
|
|
647
|
+
name = normalize_optional_skill_name(name)
|
|
648
|
+
for record in read_optional_skill_records(root, role=role, include_archived=True):
|
|
649
|
+
if record["name"] == name:
|
|
650
|
+
return record
|
|
651
|
+
raise ValueError(f"unknown optional skill for {role}: {name}")
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _optional_records_by_role(records: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
|
655
|
+
by_role: dict[str, list[dict[str, Any]]] = {role: [] for role in AGENT_SPECS}
|
|
656
|
+
for record in records:
|
|
657
|
+
by_role.setdefault(str(record.get("role") or ""), []).append(record)
|
|
658
|
+
return by_role
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def diff_agent_configuration(root: Path | str, role: str) -> dict[str, Any]:
|
|
662
|
+
root = Path(root).resolve()
|
|
663
|
+
if role not in AGENT_SPECS:
|
|
664
|
+
raise ValueError(f"Unknown subagent or role: {role}")
|
|
665
|
+
state = build_projection_state(root)
|
|
666
|
+
agent = state["agents"][role]
|
|
667
|
+
builtin = set(agent["builtin_skills"])
|
|
668
|
+
current = set(agent["projected_skills"])
|
|
669
|
+
effective = set(agent["effective_skills"])
|
|
670
|
+
return {
|
|
671
|
+
"role": role,
|
|
672
|
+
"builtin_skills": agent["builtin_skills"],
|
|
673
|
+
"projected_skills": agent["projected_skills"],
|
|
674
|
+
"effective_skills": agent["effective_skills"],
|
|
675
|
+
"pending_proposals": agent["pending_proposals"],
|
|
676
|
+
"applied_proposals": agent["applied_proposals"],
|
|
677
|
+
"missing_from_projected": sorted(effective - current),
|
|
678
|
+
"extra_projected": sorted(current - effective),
|
|
679
|
+
"pending_additions": sorted(effective - builtin),
|
|
680
|
+
"validation_errors": agent["validation_errors"],
|
|
681
|
+
"codex_file": agent["codex_file"],
|
|
682
|
+
"projection_manifest": state["projection_manifest"],
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def project_agent_configuration(
|
|
687
|
+
root: Path | str,
|
|
688
|
+
*,
|
|
689
|
+
role: str | None = None,
|
|
690
|
+
proposal_path: Path | str | None = None,
|
|
691
|
+
applied_by: str = "local",
|
|
692
|
+
generated_at: str | None = None,
|
|
693
|
+
) -> dict[str, Any]:
|
|
694
|
+
root = Path(root).resolve()
|
|
695
|
+
selected_role = role
|
|
696
|
+
proposal_record: dict[str, Any] | None = None
|
|
697
|
+
if proposal_path:
|
|
698
|
+
proposal = Path(proposal_path)
|
|
699
|
+
proposal = proposal if proposal.is_absolute() else root / proposal
|
|
700
|
+
proposal_record = read_skill_proposal(proposal, root)
|
|
701
|
+
selected_role = selected_role or proposal_record.get("target")
|
|
702
|
+
validation_errors = validate_skill_assignment(str(proposal_record.get("target", "")), str(proposal_record.get("skill", "")))
|
|
703
|
+
if validation_errors:
|
|
704
|
+
_rewrite_skill_proposal(root, proposal_record, proposal, "blocked", applied_by, validation_errors)
|
|
705
|
+
raise ValueError("; ".join(validation_errors))
|
|
706
|
+
_rewrite_skill_proposal(root, proposal_record, proposal, "applied", applied_by, [])
|
|
707
|
+
proposal_record = read_skill_proposal(proposal, root)
|
|
708
|
+
|
|
709
|
+
if selected_role and selected_role not in AGENT_SPECS:
|
|
710
|
+
raise ValueError(f"Unknown subagent or role: {selected_role}")
|
|
711
|
+
|
|
712
|
+
state = build_projection_state(root)
|
|
713
|
+
if selected_role:
|
|
714
|
+
roles_to_project = [selected_role] if selected_role != "head-manager" else []
|
|
715
|
+
else:
|
|
716
|
+
roles_to_project = [role_id for role_id in EXPECTED_SUBAGENTS]
|
|
717
|
+
for role_id in roles_to_project:
|
|
718
|
+
_project_agent_toml(
|
|
719
|
+
root,
|
|
720
|
+
role_id,
|
|
721
|
+
state["agents"][role_id]["effective_skills"],
|
|
722
|
+
state["agents"][role_id]["additional_instructions"]["body"],
|
|
723
|
+
)
|
|
724
|
+
if selected_role in {None, "head-manager"}:
|
|
725
|
+
_project_head_manager_prompt(root, state["agents"]["head-manager"]["additional_instructions"]["body"])
|
|
726
|
+
_project_root_strategy_skills(root)
|
|
727
|
+
|
|
728
|
+
refreshed = build_projection_state(root)
|
|
729
|
+
generated_at = generated_at or now_iso()
|
|
730
|
+
_write_projection_indexes(root, refreshed, applied_by, generated_at, proposal_record)
|
|
731
|
+
return build_projection_state(root)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def write_skill_proposal_file(root: Path | str, type_: str, target: str, skill: str) -> dict[str, Any]:
|
|
735
|
+
root = Path(root).resolve()
|
|
736
|
+
now = datetime.now(timezone.utc)
|
|
737
|
+
proposal_id = f"skill-{type_}-{target}-{skill}-{now.strftime('%Y%m%dT%H%M%S%fZ')}"
|
|
738
|
+
path = root / PROPOSAL_DIR / f"{sanitize_id(proposal_id)}.yaml"
|
|
739
|
+
validation_errors = validate_skill_assignment(target, skill)
|
|
740
|
+
status = "blocked" if validation_errors else "proposed"
|
|
741
|
+
fields = {
|
|
742
|
+
"id": proposal_id,
|
|
743
|
+
"type": type_,
|
|
744
|
+
"target": target,
|
|
745
|
+
"skill": skill,
|
|
746
|
+
"created_at": now.isoformat().replace("+00:00", "Z"),
|
|
747
|
+
"requires_validation": "true",
|
|
748
|
+
"status": status,
|
|
749
|
+
"validation_status": "blocked" if validation_errors else "valid",
|
|
750
|
+
}
|
|
751
|
+
if validation_errors:
|
|
752
|
+
fields["validation_error"] = "; ".join(validation_errors)
|
|
753
|
+
_write_simple_yaml(path, fields)
|
|
754
|
+
result = {"status": status, "id": proposal_id, "path": path.relative_to(root).as_posix(), "validation_errors": validation_errors}
|
|
755
|
+
return result
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def read_skill_proposals(root: Path | str) -> list[dict[str, Any]]:
|
|
759
|
+
root = Path(root).resolve()
|
|
760
|
+
proposals: list[dict[str, Any]] = []
|
|
761
|
+
for path in sorted((root / PROPOSAL_DIR).glob("*.yaml")):
|
|
762
|
+
proposals.append(read_skill_proposal(path, root))
|
|
763
|
+
return proposals
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def read_skill_proposal(path: Path, root: Path | None = None) -> dict[str, Any]:
|
|
767
|
+
root = root.resolve() if root else None
|
|
768
|
+
data = _read_simple_yaml(path)
|
|
769
|
+
data["path"] = path.relative_to(root).as_posix() if root and path.is_relative_to(root) else str(path)
|
|
770
|
+
data["source_file_hash"] = _file_hash(path)
|
|
771
|
+
return data
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def validate_skill_assignment(role: str, skill: str) -> list[str]:
|
|
775
|
+
errors: list[str] = []
|
|
776
|
+
agent = AGENT_SPECS.get(role)
|
|
777
|
+
skill_spec = SKILL_SPECS.get(skill)
|
|
778
|
+
if not agent:
|
|
779
|
+
errors.append(f"unknown role: {role}")
|
|
780
|
+
if not skill_spec:
|
|
781
|
+
errors.append(f"unknown skill: {skill}")
|
|
782
|
+
if errors or not agent or not skill_spec:
|
|
783
|
+
return errors
|
|
784
|
+
if role != "head-manager" and skill_spec.scope == "mainagent":
|
|
785
|
+
errors.append(f"{role} cannot receive project-scope mainagent skill: {skill}")
|
|
786
|
+
blocked_tags = sorted(set(agent.forbidden_skill_tags).intersection(skill_spec.risk_tags))
|
|
787
|
+
if blocked_tags:
|
|
788
|
+
errors.append(f"{role} cannot receive {skill}; blocked risk tags: {', '.join(blocked_tags)}")
|
|
789
|
+
if skill_spec.owner_roles and role not in skill_spec.owner_roles:
|
|
790
|
+
errors.append(f"{role} is not an owner role for {skill}")
|
|
791
|
+
return errors
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def skills_for_role(root: Path | str, role: str) -> list[str]:
|
|
795
|
+
return inspect_agent_configuration(root, role)["effective_skills"]
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def build_projection_state(root: Path | str) -> dict[str, Any]:
|
|
799
|
+
root = Path(root).resolve()
|
|
800
|
+
manifest = read_json(root / MANIFEST_PATH, {}) or {}
|
|
801
|
+
applied_by_role: dict[str, list[dict[str, Any]]] = {role: [] for role in AGENT_SPECS}
|
|
802
|
+
pending_by_role: dict[str, list[dict[str, Any]]] = {role: [] for role in AGENT_SPECS}
|
|
803
|
+
blocked_by_role: dict[str, list[dict[str, Any]]] = {role: [] for role in AGENT_SPECS}
|
|
804
|
+
for proposal in read_skill_proposals(root):
|
|
805
|
+
target = str(proposal.get("target", ""))
|
|
806
|
+
if target not in AGENT_SPECS:
|
|
807
|
+
continue
|
|
808
|
+
status = str(proposal.get("status", "proposed"))
|
|
809
|
+
if status == "applied":
|
|
810
|
+
applied_by_role[target].append(proposal)
|
|
811
|
+
elif status == "blocked":
|
|
812
|
+
blocked_by_role[target].append(proposal)
|
|
813
|
+
else:
|
|
814
|
+
pending_by_role[target].append(proposal)
|
|
815
|
+
|
|
816
|
+
agents: dict[str, dict[str, Any]] = {}
|
|
817
|
+
skill_root_exists = (root / MAINAGENT_SKILL_DIR).exists()
|
|
818
|
+
optional_records = read_optional_skill_records(root, include_archived=True)
|
|
819
|
+
optional_by_role = _optional_records_by_role(optional_records)
|
|
820
|
+
for role, spec in AGENT_SPECS.items():
|
|
821
|
+
applied_skills = [str(item.get("skill")) for item in applied_by_role[role] if item.get("skill")]
|
|
822
|
+
active_optional = [
|
|
823
|
+
str(record.get("name"))
|
|
824
|
+
for record in optional_by_role.get(role, [])
|
|
825
|
+
if record.get("status") == "active" and not record.get("validation_errors")
|
|
826
|
+
]
|
|
827
|
+
effective = _unique_existing(root, [*spec.builtin_skills, *applied_skills, *active_optional], role=role)
|
|
828
|
+
agent_file = _agent_config_path(root, role)
|
|
829
|
+
projected_skills = _parse_toml_skill_paths(agent_file.read_text(encoding="utf-8")) if agent_file.exists() else []
|
|
830
|
+
validation_errors: list[str] = []
|
|
831
|
+
for skill in effective:
|
|
832
|
+
if skill in SKILL_SPECS:
|
|
833
|
+
validation_errors.extend(validate_skill_assignment(role, skill))
|
|
834
|
+
for optional in optional_by_role.get(role, []):
|
|
835
|
+
validation_errors.extend(optional.get("validation_errors") or [])
|
|
836
|
+
additional = read_agent_additional_instructions(root, role)
|
|
837
|
+
agents[role] = {
|
|
838
|
+
**_agent_spec_payload(spec),
|
|
839
|
+
"codex_file": _relative_path(root, agent_file) if agent_file else "",
|
|
840
|
+
"codex_file_hash": _file_hash(agent_file) if agent_file else None,
|
|
841
|
+
"builtin_skills": list(spec.builtin_skills)
|
|
842
|
+
if not skill_root_exists
|
|
843
|
+
else [skill for skill in spec.builtin_skills if _skill_path(root, skill, role=role).exists()],
|
|
844
|
+
"effective_skills": effective,
|
|
845
|
+
"projected_skills": projected_skills,
|
|
846
|
+
"pending_proposals": [_proposal_summary(proposal) for proposal in pending_by_role[role]],
|
|
847
|
+
"applied_proposals": [_proposal_summary(proposal) for proposal in applied_by_role[role]],
|
|
848
|
+
"blocked_proposals": [_proposal_summary(proposal) for proposal in blocked_by_role[role]],
|
|
849
|
+
"optional_skills": optional_by_role.get(role, []),
|
|
850
|
+
"optional_skill_count": len([record for record in optional_by_role.get(role, []) if record.get("status") == "active"]),
|
|
851
|
+
"additional_instructions": additional,
|
|
852
|
+
"additional_instruction_count": additional["line_count"],
|
|
853
|
+
"validation_errors": sorted(set(validation_errors)),
|
|
854
|
+
"permission_profile": spec.permission_profile,
|
|
855
|
+
"mcp_allowlist": list(spec.mcp_allowlist),
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
skills = _installed_skill_index(root, optional_records)
|
|
859
|
+
projection_input = {
|
|
860
|
+
"agents": {
|
|
861
|
+
role: {
|
|
862
|
+
"effective_skills": agent["effective_skills"],
|
|
863
|
+
"codex_file_hash": agent["codex_file_hash"],
|
|
864
|
+
"additional_instructions_hash": agent["additional_instructions"]["source_file_hash"],
|
|
865
|
+
}
|
|
866
|
+
for role, agent in agents.items()
|
|
867
|
+
},
|
|
868
|
+
"skills": {skill_id: item["source_file_hash"] for skill_id, item in skills.items()},
|
|
869
|
+
"applied_proposals": {
|
|
870
|
+
role: [proposal["source_file_hash"] for proposal in agent["applied_proposals"]]
|
|
871
|
+
for role, agent in agents.items()
|
|
872
|
+
},
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
"root": str(root),
|
|
876
|
+
"registry": "tradingcodex_service.application.agents",
|
|
877
|
+
"agents": agents,
|
|
878
|
+
"skills": skills,
|
|
879
|
+
"projection_hash": stable_hash(projection_input),
|
|
880
|
+
"projection_manifest": manifest,
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _write_projection_indexes(
|
|
885
|
+
root: Path,
|
|
886
|
+
state: dict[str, Any],
|
|
887
|
+
applied_by: str,
|
|
888
|
+
generated_at: str,
|
|
889
|
+
proposal_record: dict[str, Any] | None,
|
|
890
|
+
) -> None:
|
|
891
|
+
agent_index = {
|
|
892
|
+
"generated_at": generated_at,
|
|
893
|
+
"source": "tradingcodex_service.application.agents",
|
|
894
|
+
"projection_hash": state["projection_hash"],
|
|
895
|
+
"agents": state["agents"],
|
|
896
|
+
}
|
|
897
|
+
skill_index = {
|
|
898
|
+
"generated_at": generated_at,
|
|
899
|
+
"source": "workspace-files",
|
|
900
|
+
"projection_hash": state["projection_hash"],
|
|
901
|
+
"skills": state["skills"],
|
|
902
|
+
}
|
|
903
|
+
manifest_roles = []
|
|
904
|
+
for role, agent in state["agents"].items():
|
|
905
|
+
manifest_roles.append(
|
|
906
|
+
{
|
|
907
|
+
"role": role,
|
|
908
|
+
"codex_file": agent["codex_file"],
|
|
909
|
+
"source_file_hash": agent["codex_file_hash"],
|
|
910
|
+
"effective_skills": [
|
|
911
|
+
{
|
|
912
|
+
"skill": skill,
|
|
913
|
+
"source_file": state["skills"].get(skill, {}).get("source_file", ""),
|
|
914
|
+
"source_file_hash": state["skills"].get(skill, {}).get("source_file_hash"),
|
|
915
|
+
}
|
|
916
|
+
for skill in agent["effective_skills"]
|
|
917
|
+
],
|
|
918
|
+
"additional_instructions": {
|
|
919
|
+
"source_file": agent["additional_instructions"]["source_file"],
|
|
920
|
+
"source_file_hash": agent["additional_instructions"]["source_file_hash"],
|
|
921
|
+
"line_count": agent["additional_instructions"]["line_count"],
|
|
922
|
+
},
|
|
923
|
+
}
|
|
924
|
+
)
|
|
925
|
+
manifest = {
|
|
926
|
+
"generated_at": generated_at,
|
|
927
|
+
"applied_by": applied_by,
|
|
928
|
+
"projection_hash": state["projection_hash"],
|
|
929
|
+
"source": "file-native-agent-skill-projection",
|
|
930
|
+
"proposal": _proposal_summary(proposal_record) if proposal_record else None,
|
|
931
|
+
"roles": manifest_roles,
|
|
932
|
+
}
|
|
933
|
+
write_json(root / AGENT_INDEX_PATH, agent_index)
|
|
934
|
+
write_json(root / SKILL_INDEX_PATH, skill_index)
|
|
935
|
+
write_json(root / MANIFEST_PATH, manifest)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _optional_record_payload(root: Path, record: dict[str, Any]) -> dict[str, Any]:
|
|
939
|
+
role = str(record.get("role") or "")
|
|
940
|
+
name = normalize_optional_skill_name(str(record.get("name") or ""))
|
|
941
|
+
source_file = str(record.get("source_file") or "")
|
|
942
|
+
metadata_file = str(record.get("metadata_file") or "")
|
|
943
|
+
status_file = str(record.get("status_file") or "")
|
|
944
|
+
skill_path = root / source_file if source_file else _skill_path(root, name, role=role)
|
|
945
|
+
metadata_path = root / metadata_file if metadata_file else skill_path.parent / "agents" / "openai.yaml"
|
|
946
|
+
status_path = root / status_file if status_file else skill_path.parent / OPTIONAL_SKILL_STATUS_FILE
|
|
947
|
+
fields = _read_frontmatter_fields(skill_path)
|
|
948
|
+
body = _safe_read(skill_path)
|
|
949
|
+
validation = validate_optional_skill_payload(
|
|
950
|
+
role,
|
|
951
|
+
name,
|
|
952
|
+
str(fields.get("description") or ""),
|
|
953
|
+
body,
|
|
954
|
+
)
|
|
955
|
+
if "name" not in fields:
|
|
956
|
+
validation["errors"].append("missing optional skill frontmatter: name")
|
|
957
|
+
elif fields.get("name") != name:
|
|
958
|
+
validation["errors"].append("optional skill frontmatter name must match directory name")
|
|
959
|
+
if "description" not in fields:
|
|
960
|
+
validation["errors"].append("missing optional skill frontmatter: description")
|
|
961
|
+
status = str(record.get("status") or "active")
|
|
962
|
+
if status not in OPTIONAL_SKILL_STATUSES:
|
|
963
|
+
validation["errors"].append(f"unknown optional skill status: {status}")
|
|
964
|
+
payload = {
|
|
965
|
+
"role": role,
|
|
966
|
+
"name": name,
|
|
967
|
+
"description": fields.get("description") or "",
|
|
968
|
+
"status": status,
|
|
969
|
+
"source": "optional",
|
|
970
|
+
"scope": str(record.get("scope") or "role"),
|
|
971
|
+
"core": False,
|
|
972
|
+
"installed": skill_path.exists(),
|
|
973
|
+
"source_file": _relative_path(root, skill_path),
|
|
974
|
+
"source_file_hash": _file_hash(skill_path),
|
|
975
|
+
"metadata_file": _relative_path(root, metadata_path),
|
|
976
|
+
"metadata_file_hash": _file_hash(metadata_path),
|
|
977
|
+
"status_file": _relative_path(root, status_path),
|
|
978
|
+
"status_file_hash": _file_hash(status_path),
|
|
979
|
+
"validation_status": "blocked" if validation["errors"] else "valid",
|
|
980
|
+
"validation_errors": validation["errors"],
|
|
981
|
+
"risk_tags": validation["risk_tags"],
|
|
982
|
+
"frontmatter": fields,
|
|
983
|
+
}
|
|
984
|
+
for key in ("created_by", "created_at", "updated_by", "updated_at", "roles"):
|
|
985
|
+
if key in record:
|
|
986
|
+
payload[key] = record[key]
|
|
987
|
+
return payload
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _strategy_record_payload(root: Path, skill_path: Path) -> dict[str, Any]:
|
|
991
|
+
name = skill_path.parent.name
|
|
992
|
+
metadata_path = skill_path.parent / "agents" / "openai.yaml"
|
|
993
|
+
fields = _read_frontmatter_fields(skill_path)
|
|
994
|
+
missing = sorted(STRATEGY_REQUIRED_FRONTMATTER - set(fields))
|
|
995
|
+
validation_errors: list[str] = []
|
|
996
|
+
if missing:
|
|
997
|
+
validation_errors.append(f"missing strategy frontmatter: {', '.join(missing)}")
|
|
998
|
+
if not name.startswith(STRATEGY_SKILL_PREFIX):
|
|
999
|
+
validation_errors.append("strategy skill name must start with strategy-")
|
|
1000
|
+
if fields.get("name") and fields.get("name") != name:
|
|
1001
|
+
validation_errors.append("strategy skill frontmatter name must match directory name")
|
|
1002
|
+
if fields.get("type") and fields.get("type") != "strategy":
|
|
1003
|
+
validation_errors.append("strategy skill frontmatter type must be strategy")
|
|
1004
|
+
if fields.get("managed_by") and fields.get("managed_by") != "strategy-creator":
|
|
1005
|
+
validation_errors.append("strategy skill frontmatter managed_by must be strategy-creator")
|
|
1006
|
+
body = _safe_read(skill_path)
|
|
1007
|
+
missing_sections = [section for section in STRATEGY_REQUIRED_SECTIONS if section not in body]
|
|
1008
|
+
if missing_sections:
|
|
1009
|
+
validation_errors.append(f"missing strategy sections: {', '.join(missing_sections)}")
|
|
1010
|
+
status = fields.get("status") or "unknown"
|
|
1011
|
+
active = status == "active" and not validation_errors
|
|
1012
|
+
return {
|
|
1013
|
+
"name": name,
|
|
1014
|
+
"description": fields.get("description") or "",
|
|
1015
|
+
"label": name,
|
|
1016
|
+
"owner_roles": ["head-manager"],
|
|
1017
|
+
"risk_tags": ["strategy"],
|
|
1018
|
+
"user_visible": active,
|
|
1019
|
+
"source": "strategy",
|
|
1020
|
+
"scope": "strategy",
|
|
1021
|
+
"core": False,
|
|
1022
|
+
"status": status,
|
|
1023
|
+
"active": active,
|
|
1024
|
+
"installed": skill_path.exists(),
|
|
1025
|
+
"source_file": _relative_path(root, skill_path),
|
|
1026
|
+
"source_file_hash": _file_hash(skill_path),
|
|
1027
|
+
"metadata_file": _relative_path(root, metadata_path),
|
|
1028
|
+
"metadata_file_hash": _file_hash(metadata_path),
|
|
1029
|
+
"validation_status": "blocked" if validation_errors else "valid",
|
|
1030
|
+
"validation_errors": validation_errors,
|
|
1031
|
+
"frontmatter": fields,
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _project_agent_toml(root: Path, role: str, skills: list[str], additional_instructions: str = "") -> None:
|
|
1036
|
+
path = _agent_config_path(root, role)
|
|
1037
|
+
if not path.exists():
|
|
1038
|
+
return
|
|
1039
|
+
text = path.read_text(encoding="utf-8")
|
|
1040
|
+
marker = "[[skills.config]]"
|
|
1041
|
+
body = text[: text.find(marker)].rstrip() if marker in text else text.rstrip()
|
|
1042
|
+
body = _replace_developer_instructions(body, additional_instructions)
|
|
1043
|
+
rendered = body + "\n\n" + _render_skill_config_blocks(root, skills, role=role)
|
|
1044
|
+
path.write_text(rendered.rstrip() + "\n", encoding="utf-8")
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def _project_head_manager_prompt(root: Path, additional_instructions: str = "") -> None:
|
|
1048
|
+
path = root / ".codex" / "prompts" / "base_instructions" / "head-manager.md"
|
|
1049
|
+
if not path.exists():
|
|
1050
|
+
return
|
|
1051
|
+
text = _strip_additional_instruction_block(path.read_text(encoding="utf-8")).rstrip()
|
|
1052
|
+
if additional_instructions.strip():
|
|
1053
|
+
text += "\n\n" + _render_additional_instruction_block(additional_instructions).rstrip()
|
|
1054
|
+
path.write_text(text.rstrip() + "\n", encoding="utf-8")
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _replace_developer_instructions(text: str, additional_instructions: str) -> str:
|
|
1058
|
+
pattern = re.compile(r'developer_instructions\s*=\s*"""(?P<body>.*?)"""', re.S)
|
|
1059
|
+
match = pattern.search(text)
|
|
1060
|
+
if not match:
|
|
1061
|
+
return text
|
|
1062
|
+
base = _strip_additional_instruction_block(match.group("body")).strip("\n")
|
|
1063
|
+
if additional_instructions.strip():
|
|
1064
|
+
block = _escape_toml_multiline_basic(_render_additional_instruction_block(additional_instructions).rstrip())
|
|
1065
|
+
base += "\n\n" + block
|
|
1066
|
+
rendered = 'developer_instructions = """\n' + base + '\n"""'
|
|
1067
|
+
return text[: match.start()] + rendered + text[match.end() :]
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _render_additional_instruction_block(additional_instructions: str) -> str:
|
|
1071
|
+
return "\n".join(
|
|
1072
|
+
[
|
|
1073
|
+
ADDITIONAL_INSTRUCTION_START,
|
|
1074
|
+
"",
|
|
1075
|
+
"These project-local instructions are appended after the generated default role instructions.",
|
|
1076
|
+
"",
|
|
1077
|
+
additional_instructions.strip(),
|
|
1078
|
+
"",
|
|
1079
|
+
ADDITIONAL_INSTRUCTION_END,
|
|
1080
|
+
"",
|
|
1081
|
+
]
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _strip_additional_instruction_block(text: str) -> str:
|
|
1086
|
+
pattern = re.compile(
|
|
1087
|
+
rf"\n*{re.escape(ADDITIONAL_INSTRUCTION_START)}.*?{re.escape(ADDITIONAL_INSTRUCTION_END)}\n*",
|
|
1088
|
+
re.S,
|
|
1089
|
+
)
|
|
1090
|
+
return pattern.sub("\n", text)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _escape_toml_multiline_basic(text: str) -> str:
|
|
1094
|
+
return text.replace("\\", "\\\\").replace('"""', '\\"\\"\\"')
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _project_root_strategy_skills(root: Path) -> None:
|
|
1098
|
+
path = _agent_config_path(root, "head-manager")
|
|
1099
|
+
if not path.exists():
|
|
1100
|
+
return
|
|
1101
|
+
text = path.read_text(encoding="utf-8")
|
|
1102
|
+
strategy_records = read_strategy_skill_records(root, active_only=True)
|
|
1103
|
+
rendered = "\n".join(
|
|
1104
|
+
f'[[skills.config]]\npath = "{(root / str(record["source_file"])).as_posix()}"\nenabled = true'
|
|
1105
|
+
for record in strategy_records
|
|
1106
|
+
)
|
|
1107
|
+
block = f"{STRATEGY_ROOT_CONFIG_START}\n{rendered}\n{STRATEGY_ROOT_CONFIG_END}".replace("\n\n", "\n")
|
|
1108
|
+
if STRATEGY_ROOT_CONFIG_START in text and STRATEGY_ROOT_CONFIG_END in text:
|
|
1109
|
+
pattern = re.compile(
|
|
1110
|
+
rf"{re.escape(STRATEGY_ROOT_CONFIG_START)}.*?{re.escape(STRATEGY_ROOT_CONFIG_END)}",
|
|
1111
|
+
re.S,
|
|
1112
|
+
)
|
|
1113
|
+
updated = pattern.sub(block, text)
|
|
1114
|
+
else:
|
|
1115
|
+
permissions_index = text.find("\n[permissions.")
|
|
1116
|
+
if permissions_index >= 0:
|
|
1117
|
+
updated = text[:permissions_index].rstrip() + "\n\n" + block + "\n" + text[permissions_index:]
|
|
1118
|
+
else:
|
|
1119
|
+
updated = text.rstrip() + "\n\n" + block + "\n"
|
|
1120
|
+
if updated != text:
|
|
1121
|
+
path.write_text(updated.rstrip() + "\n", encoding="utf-8")
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def _render_skill_config_blocks(root: Path, skills: list[str], *, role: str | None = None) -> str:
|
|
1125
|
+
blocks = []
|
|
1126
|
+
for skill in skills:
|
|
1127
|
+
skill_path = _skill_path(root, skill, role=role)
|
|
1128
|
+
blocks.append(f'[[skills.config]]\npath = "{skill_path.as_posix()}"\nenabled = true')
|
|
1129
|
+
return "\n\n".join(blocks) + ("\n" if blocks else "")
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _rewrite_skill_proposal(root: Path, proposal: dict[str, Any], path: Path, status: str, applied_by: str, errors: list[str]) -> None:
|
|
1133
|
+
fields = {
|
|
1134
|
+
"id": proposal.get("id", path.stem),
|
|
1135
|
+
"type": proposal.get("type", "add"),
|
|
1136
|
+
"target": proposal.get("target", ""),
|
|
1137
|
+
"skill": proposal.get("skill", ""),
|
|
1138
|
+
"created_at": proposal.get("created_at", ""),
|
|
1139
|
+
"requires_validation": proposal.get("requires_validation", "true"),
|
|
1140
|
+
"status": status,
|
|
1141
|
+
"validation_status": "blocked" if errors else "valid",
|
|
1142
|
+
"applied_by": applied_by,
|
|
1143
|
+
"applied_at": now_iso(),
|
|
1144
|
+
}
|
|
1145
|
+
if errors:
|
|
1146
|
+
fields["validation_error"] = "; ".join(errors)
|
|
1147
|
+
_write_simple_yaml(path, fields)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _installed_skill_index(root: Path, optional_records: list[dict[str, Any]] | None = None) -> dict[str, dict[str, Any]]:
|
|
1151
|
+
skills: dict[str, dict[str, Any]] = {}
|
|
1152
|
+
for skill_id, spec in SKILL_SPECS.items():
|
|
1153
|
+
skill_path = _skill_path(root, skill_id)
|
|
1154
|
+
meta_path = skill_path.parent / "agents" / "openai.yaml"
|
|
1155
|
+
skills[skill_id] = {
|
|
1156
|
+
"id": skill_id,
|
|
1157
|
+
"label": spec.label,
|
|
1158
|
+
"owner_roles": list(spec.owner_roles),
|
|
1159
|
+
"risk_tags": list(spec.risk_tags),
|
|
1160
|
+
"user_visible": spec.user_visible,
|
|
1161
|
+
"source": "core",
|
|
1162
|
+
"scope": spec.scope,
|
|
1163
|
+
"core": True,
|
|
1164
|
+
"installed": skill_path.exists(),
|
|
1165
|
+
"source_file": _relative_path(root, skill_path),
|
|
1166
|
+
"source_file_hash": _file_hash(skill_path),
|
|
1167
|
+
"metadata_file": _relative_path(root, meta_path),
|
|
1168
|
+
"metadata_file_hash": _file_hash(meta_path),
|
|
1169
|
+
}
|
|
1170
|
+
for record in read_strategy_skill_records(root):
|
|
1171
|
+
skill_id = str(record.get("name") or "")
|
|
1172
|
+
if not skill_id or skill_id in skills:
|
|
1173
|
+
continue
|
|
1174
|
+
skills[skill_id] = record
|
|
1175
|
+
for record in optional_records or read_optional_skill_records(root, include_archived=True):
|
|
1176
|
+
skill_name = str(record.get("name") or "")
|
|
1177
|
+
if not skill_name or skill_name in skills:
|
|
1178
|
+
continue
|
|
1179
|
+
skills[skill_name] = {
|
|
1180
|
+
"name": skill_name,
|
|
1181
|
+
"label": skill_name,
|
|
1182
|
+
"description": str(record.get("description") or ""),
|
|
1183
|
+
"owner_roles": [record.get("role")],
|
|
1184
|
+
"risk_tags": list(record.get("risk_tags") or []),
|
|
1185
|
+
"user_visible": False,
|
|
1186
|
+
"source": "optional",
|
|
1187
|
+
"scope": record.get("scope", "role"),
|
|
1188
|
+
"core": False,
|
|
1189
|
+
"status": record.get("status"),
|
|
1190
|
+
"installed": bool(record.get("installed")),
|
|
1191
|
+
"source_file": record.get("source_file", ""),
|
|
1192
|
+
"source_file_hash": record.get("source_file_hash"),
|
|
1193
|
+
"metadata_file": record.get("metadata_file", ""),
|
|
1194
|
+
"metadata_file_hash": record.get("metadata_file_hash"),
|
|
1195
|
+
"status_file": record.get("status_file", ""),
|
|
1196
|
+
"status_file_hash": record.get("status_file_hash"),
|
|
1197
|
+
"validation_status": record.get("validation_status"),
|
|
1198
|
+
"validation_errors": record.get("validation_errors", []),
|
|
1199
|
+
}
|
|
1200
|
+
return skills
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _agent_spec_payload(spec: AgentSpec) -> dict[str, Any]:
|
|
1204
|
+
return {
|
|
1205
|
+
"role": spec.role,
|
|
1206
|
+
"label": spec.label,
|
|
1207
|
+
"group": spec.group,
|
|
1208
|
+
"permission_profile": spec.permission_profile,
|
|
1209
|
+
"builtin_skills": list(spec.builtin_skills),
|
|
1210
|
+
"forbidden_skill_tags": list(spec.forbidden_skill_tags),
|
|
1211
|
+
"mcp_allowlist": list(spec.mcp_allowlist),
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _agent_config_path(root: Path, role: str) -> Path:
|
|
1216
|
+
if role == "head-manager":
|
|
1217
|
+
return root / ".codex" / "config.toml"
|
|
1218
|
+
return root / ".codex" / "agents" / f"{role}.toml"
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _additional_instruction_path(root: Path, role: str) -> Path:
|
|
1222
|
+
return root / ADDITIONAL_INSTRUCTION_DIR / f"{role}.md"
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _skill_path(root: Path, skill: str, *, role: str | None = None) -> Path:
|
|
1226
|
+
spec = SKILL_SPECS.get(skill)
|
|
1227
|
+
if spec:
|
|
1228
|
+
if spec.scope == "subagent_shared":
|
|
1229
|
+
return root / SUBAGENT_SHARED_SKILL_DIR / skill / "SKILL.md"
|
|
1230
|
+
if spec.scope == "subagent_role":
|
|
1231
|
+
target_role = role or (spec.owner_roles[0] if spec.owner_roles else "")
|
|
1232
|
+
return root / SUBAGENT_SKILL_DIR / target_role / skill / "SKILL.md"
|
|
1233
|
+
return root / MAINAGENT_SKILL_DIR / skill / "SKILL.md"
|
|
1234
|
+
if role:
|
|
1235
|
+
role_path = root / SUBAGENT_SKILL_DIR / role / skill / "SKILL.md"
|
|
1236
|
+
if role_path.exists():
|
|
1237
|
+
return role_path
|
|
1238
|
+
shared_path = root / SUBAGENT_SHARED_SKILL_DIR / skill / "SKILL.md"
|
|
1239
|
+
if shared_path.exists():
|
|
1240
|
+
return shared_path
|
|
1241
|
+
return role_path
|
|
1242
|
+
for candidate in sorted((root / SUBAGENT_SKILL_DIR).glob(f"*/{skill}/SKILL.md")):
|
|
1243
|
+
return candidate
|
|
1244
|
+
return root / MAINAGENT_SKILL_DIR / skill / "SKILL.md"
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def _parse_toml_skill_paths(text: str) -> list[str]:
|
|
1248
|
+
skills: list[str] = []
|
|
1249
|
+
patterns = [
|
|
1250
|
+
r'path\s*=\s*"[^"]+/\.agents/skills/([^"/]+)/SKILL\.md"',
|
|
1251
|
+
r'path\s*=\s*"[^"]+/\.tradingcodex/subagents/skills/(?:shared|[^"/]+)/([^"/]+)/SKILL\.md"',
|
|
1252
|
+
r'path\s*=\s*"[^"]+/\.tradingcodex/strategies/([^"/]+)/SKILL\.md"',
|
|
1253
|
+
]
|
|
1254
|
+
for pattern in patterns:
|
|
1255
|
+
for match in re.finditer(pattern, text):
|
|
1256
|
+
skills.append(match.group(1))
|
|
1257
|
+
return list(dict.fromkeys(skills))
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _proposal_summary(proposal: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
1261
|
+
if proposal is None:
|
|
1262
|
+
return None
|
|
1263
|
+
return {
|
|
1264
|
+
"id": proposal.get("id"),
|
|
1265
|
+
"type": proposal.get("type"),
|
|
1266
|
+
"target": proposal.get("target"),
|
|
1267
|
+
"skill": proposal.get("skill"),
|
|
1268
|
+
"status": proposal.get("status"),
|
|
1269
|
+
"path": proposal.get("path"),
|
|
1270
|
+
"source_file_hash": proposal.get("source_file_hash"),
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
def _file_hash(path: Path | None) -> str | None:
|
|
1275
|
+
if path is None or not path.exists() or not path.is_file():
|
|
1276
|
+
return None
|
|
1277
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def _relative_path(root: Path, path: Path) -> str:
|
|
1281
|
+
try:
|
|
1282
|
+
return path.relative_to(root).as_posix()
|
|
1283
|
+
except ValueError:
|
|
1284
|
+
return str(path)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def _optional_record_roles(record: dict[str, Any], scope_name: str) -> list[str]:
|
|
1288
|
+
if scope_name != "shared":
|
|
1289
|
+
return [scope_name]
|
|
1290
|
+
roles = record.get("roles")
|
|
1291
|
+
if isinstance(roles, list):
|
|
1292
|
+
return [str(role) for role in roles if str(role) in EXPECTED_SUBAGENTS]
|
|
1293
|
+
if isinstance(roles, str):
|
|
1294
|
+
parsed = [item.strip() for item in roles.split(",") if item.strip()]
|
|
1295
|
+
return [item for item in parsed if item in EXPECTED_SUBAGENTS]
|
|
1296
|
+
role = str(record.get("role") or "")
|
|
1297
|
+
if role in EXPECTED_SUBAGENTS:
|
|
1298
|
+
return [role]
|
|
1299
|
+
return list(EXPECTED_SUBAGENTS)
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def _unique_existing(root: Path, skills: list[str], *, role: str | None = None) -> list[str]:
|
|
1303
|
+
unique = list(dict.fromkeys(skills))
|
|
1304
|
+
if not (root / MAINAGENT_SKILL_DIR).exists() and not (root / SUBAGENT_SKILL_DIR).exists():
|
|
1305
|
+
return unique
|
|
1306
|
+
return [skill for skill in unique if _skill_path(root, skill, role=role).exists()]
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def _atomic_write_text(path: Path, text: str) -> None:
|
|
1310
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1311
|
+
temp_path = path.with_name(f".{path.name}.tmp")
|
|
1312
|
+
temp_path.write_text(text, encoding="utf-8")
|
|
1313
|
+
temp_path.replace(path)
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
def _atomic_write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
1317
|
+
_atomic_write_text(path, json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False) + "\n")
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def _read_markdown_body(path: Path) -> str:
|
|
1321
|
+
try:
|
|
1322
|
+
text = path.read_text(encoding="utf-8")
|
|
1323
|
+
except Exception:
|
|
1324
|
+
return ""
|
|
1325
|
+
if not text.startswith("---\n"):
|
|
1326
|
+
return text
|
|
1327
|
+
end = text.find("\n---", 4)
|
|
1328
|
+
if end < 0:
|
|
1329
|
+
return text
|
|
1330
|
+
body_start = text.find("\n", end + 4)
|
|
1331
|
+
return text[body_start + 1 :] if body_start >= 0 else ""
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def _render_basic_skill_markdown(name: str, description: str, body: str) -> str:
|
|
1335
|
+
frontmatter = {
|
|
1336
|
+
"name": name,
|
|
1337
|
+
"description": description or name.replace("-", " ").title(),
|
|
1338
|
+
}
|
|
1339
|
+
return _render_frontmatter(frontmatter) + body.strip() + "\n"
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def _render_strategy_skill_markdown(name: str, description: str, body: str, language: str, status: str) -> str:
|
|
1343
|
+
frontmatter = {
|
|
1344
|
+
"name": name,
|
|
1345
|
+
"description": description,
|
|
1346
|
+
"type": "strategy",
|
|
1347
|
+
"status": status,
|
|
1348
|
+
"language": language or "unknown",
|
|
1349
|
+
"managed_by": "strategy-creator",
|
|
1350
|
+
"owner": "user",
|
|
1351
|
+
"last_reviewed": now_iso()[:10],
|
|
1352
|
+
}
|
|
1353
|
+
return _render_frontmatter(frontmatter) + _ensure_strategy_sections(name, body).strip() + "\n"
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def _render_frontmatter(fields: dict[str, Any]) -> str:
|
|
1357
|
+
lines = ["---"]
|
|
1358
|
+
for key, value in fields.items():
|
|
1359
|
+
lines.append(f"{key}: {_yaml_scalar(value)}")
|
|
1360
|
+
lines.append("---")
|
|
1361
|
+
lines.append("")
|
|
1362
|
+
return "\n".join(lines)
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def _ensure_strategy_sections(name: str, body: str) -> str:
|
|
1366
|
+
text = body.strip() or _default_strategy_body(name)
|
|
1367
|
+
if not text.startswith("# "):
|
|
1368
|
+
text = f"# {_strategy_display_name(name, text)}\n\n{text}"
|
|
1369
|
+
for section in STRATEGY_REQUIRED_SECTIONS:
|
|
1370
|
+
if section not in text:
|
|
1371
|
+
text = text.rstrip() + f"\n\n{section}\nnot specified\n"
|
|
1372
|
+
return text
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def _default_strategy_body(name: str) -> str:
|
|
1376
|
+
sections = "\n\n".join(f"{section}\nnot specified" for section in STRATEGY_REQUIRED_SECTIONS)
|
|
1377
|
+
return f"# {_strategy_display_name(name, '')}\n\n{sections}\n"
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def _strategy_display_name(name: str, body: str) -> str:
|
|
1381
|
+
return _skill_display_name(name.removeprefix(STRATEGY_SKILL_PREFIX), body) if name.startswith(STRATEGY_SKILL_PREFIX) else _skill_display_name(name, body)
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def _skill_display_name(name: str, body: str) -> str:
|
|
1385
|
+
for line in body.splitlines():
|
|
1386
|
+
stripped = line.strip()
|
|
1387
|
+
if stripped.startswith("# "):
|
|
1388
|
+
heading = stripped[2:].strip()
|
|
1389
|
+
if heading:
|
|
1390
|
+
return heading
|
|
1391
|
+
return name.replace("-", " ").title() or name
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def _render_openai_yaml(display_name: str, short_description: str, default_prompt: str) -> str:
|
|
1395
|
+
short = re.sub(r"\s+", " ", short_description or display_name).strip()
|
|
1396
|
+
if len(short) < 25:
|
|
1397
|
+
short = (short + " for TradingCodex workflows").strip()
|
|
1398
|
+
if len(short) > 64:
|
|
1399
|
+
short = short[:64].rstrip()
|
|
1400
|
+
return "\n".join(
|
|
1401
|
+
[
|
|
1402
|
+
"interface:",
|
|
1403
|
+
f" display_name: {_yaml_scalar(display_name)}",
|
|
1404
|
+
f" short_description: {_yaml_scalar(short)}",
|
|
1405
|
+
f" default_prompt: {_yaml_scalar(default_prompt)}",
|
|
1406
|
+
"policy:",
|
|
1407
|
+
" allow_implicit_invocation: true",
|
|
1408
|
+
"",
|
|
1409
|
+
]
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
def _write_simple_yaml(path: Path, fields: dict[str, Any]) -> None:
|
|
1414
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1415
|
+
lines = []
|
|
1416
|
+
for key, value in fields.items():
|
|
1417
|
+
if value is None:
|
|
1418
|
+
continue
|
|
1419
|
+
lines.append(f"{key}: {_yaml_scalar(value)}")
|
|
1420
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def _read_simple_yaml(path: Path) -> dict[str, str]:
|
|
1424
|
+
data: dict[str, str] = {}
|
|
1425
|
+
try:
|
|
1426
|
+
text = path.read_text(encoding="utf-8")
|
|
1427
|
+
except Exception:
|
|
1428
|
+
return data
|
|
1429
|
+
for line in text.splitlines():
|
|
1430
|
+
stripped = line.strip()
|
|
1431
|
+
if not stripped or stripped.startswith("#") or ":" not in stripped:
|
|
1432
|
+
continue
|
|
1433
|
+
key, raw_value = stripped.split(":", 1)
|
|
1434
|
+
data[key.strip()] = _unquote_yaml_scalar(raw_value.strip())
|
|
1435
|
+
return data
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def _read_frontmatter_fields(path: Path) -> dict[str, str]:
|
|
1439
|
+
try:
|
|
1440
|
+
text = path.read_text(encoding="utf-8")
|
|
1441
|
+
except Exception:
|
|
1442
|
+
return {}
|
|
1443
|
+
lines = text.splitlines()
|
|
1444
|
+
if not lines or lines[0].strip() != "---":
|
|
1445
|
+
return {}
|
|
1446
|
+
fields: dict[str, str] = {}
|
|
1447
|
+
for line in lines[1:]:
|
|
1448
|
+
stripped = line.strip()
|
|
1449
|
+
if stripped == "---":
|
|
1450
|
+
break
|
|
1451
|
+
if not stripped or stripped.startswith("#") or ":" not in stripped:
|
|
1452
|
+
continue
|
|
1453
|
+
key, raw_value = stripped.split(":", 1)
|
|
1454
|
+
fields[key.strip()] = _unquote_yaml_scalar(raw_value.strip())
|
|
1455
|
+
return fields
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
def _yaml_scalar(value: Any) -> str:
|
|
1459
|
+
text = str(value)
|
|
1460
|
+
if text in {"true", "false"}:
|
|
1461
|
+
return text
|
|
1462
|
+
if re.fullmatch(r"[A-Za-z0-9._/@:-]+", text):
|
|
1463
|
+
return text
|
|
1464
|
+
return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _unquote_yaml_scalar(value: str) -> str:
|
|
1468
|
+
if len(value) >= 2 and value[0] == value[-1] == '"':
|
|
1469
|
+
return value[1:-1].replace('\\"', '"').replace("\\\\", "\\")
|
|
1470
|
+
return value
|