feed-the-machine 1.6.1 → 1.7.0
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.
- package/LICENSE +21 -21
- package/README.md +170 -170
- package/bin/brain.py +1340 -0
- package/bin/convert_claude_skills_to_codex.py +490 -0
- package/bin/generate-manifest.mjs +463 -463
- package/bin/harden_codex_skills.py +141 -0
- package/bin/install.mjs +491 -491
- package/bin/migrate-eng-buddy-data.py +875 -0
- package/bin/playbook_engine/__init__.py +1 -0
- package/bin/playbook_engine/conftest.py +8 -0
- package/bin/playbook_engine/extractor.py +33 -0
- package/bin/playbook_engine/manager.py +102 -0
- package/bin/playbook_engine/models.py +84 -0
- package/bin/playbook_engine/registry.py +35 -0
- package/bin/playbook_engine/test_extractor.py +72 -0
- package/bin/playbook_engine/test_integration.py +129 -0
- package/bin/playbook_engine/test_manager.py +85 -0
- package/bin/playbook_engine/test_models.py +166 -0
- package/bin/playbook_engine/test_registry.py +67 -0
- package/bin/playbook_engine/test_tracer.py +86 -0
- package/bin/playbook_engine/tracer.py +93 -0
- package/bin/tasks_db.py +456 -0
- package/docs/HOOKS.md +243 -243
- package/docs/INBOX.md +233 -233
- package/ftm/SKILL.md +125 -122
- package/ftm-audit/SKILL.md +623 -623
- package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
- package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
- package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
- package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
- package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
- package/ftm-audit/scripts/run-knip.sh +23 -23
- package/ftm-audit.yml +2 -2
- package/ftm-brainstorm/SKILL.md +1003 -498
- package/ftm-brainstorm/evals/evals.json +180 -100
- package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
- package/ftm-brainstorm/references/agent-prompts.md +552 -224
- package/ftm-brainstorm/references/plan-template.md +209 -121
- package/ftm-brainstorm.yml +2 -2
- package/ftm-browse/SKILL.md +454 -454
- package/ftm-browse/daemon/browser-manager.ts +206 -206
- package/ftm-browse/daemon/bun.lock +30 -30
- package/ftm-browse/daemon/cli.ts +347 -347
- package/ftm-browse/daemon/commands.ts +410 -410
- package/ftm-browse/daemon/main.ts +357 -357
- package/ftm-browse/daemon/package.json +17 -17
- package/ftm-browse/daemon/server.ts +189 -189
- package/ftm-browse/daemon/snapshot.ts +519 -519
- package/ftm-browse/daemon/tsconfig.json +22 -22
- package/ftm-browse.yml +4 -4
- package/ftm-capture/SKILL.md +370 -370
- package/ftm-capture.yml +4 -4
- package/ftm-codex-gate/SKILL.md +361 -361
- package/ftm-codex-gate.yml +2 -2
- package/ftm-config/SKILL.md +422 -345
- package/ftm-config.default.yml +125 -82
- package/ftm-config.yml +44 -2
- package/ftm-council/SKILL.md +416 -416
- package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
- package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
- package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
- package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
- package/ftm-council.yml +2 -2
- package/ftm-dashboard/SKILL.md +163 -163
- package/ftm-dashboard.yml +4 -4
- package/ftm-debug/SKILL.md +1037 -1037
- package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
- package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
- package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
- package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
- package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
- package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
- package/ftm-debug.yml +2 -2
- package/ftm-diagram/SKILL.md +277 -277
- package/ftm-diagram.yml +2 -2
- package/ftm-executor/SKILL.md +777 -777
- package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
- package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
- package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
- package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
- package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
- package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
- package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
- package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
- package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
- package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
- package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
- package/ftm-executor/runtime/package.json +8 -8
- package/ftm-executor.yml +2 -2
- package/ftm-git/SKILL.md +441 -441
- package/ftm-git/evals/evals.json +26 -26
- package/ftm-git/evals/promptfoo.yaml +75 -75
- package/ftm-git/hooks/post-commit-experience.sh +92 -92
- package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
- package/ftm-git/references/protocols/REMEDIATION.md +139 -139
- package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
- package/ftm-git.yml +2 -2
- package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -64
- package/ftm-inbox/backend/adapters/base.py +230 -230
- package/ftm-inbox/backend/adapters/freshservice.py +104 -104
- package/ftm-inbox/backend/adapters/gmail.py +125 -125
- package/ftm-inbox/backend/adapters/jira.py +136 -136
- package/ftm-inbox/backend/adapters/registry.py +192 -192
- package/ftm-inbox/backend/adapters/slack.py +110 -110
- package/ftm-inbox/backend/db/connection.py +54 -54
- package/ftm-inbox/backend/db/schema.py +78 -78
- package/ftm-inbox/backend/executor/__init__.py +7 -7
- package/ftm-inbox/backend/executor/engine.py +149 -149
- package/ftm-inbox/backend/executor/step_runner.py +98 -98
- package/ftm-inbox/backend/main.py +103 -103
- package/ftm-inbox/backend/models/__init__.py +1 -1
- package/ftm-inbox/backend/models/unified_task.py +36 -36
- package/ftm-inbox/backend/planner/__init__.py +6 -6
- package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/generator.py +127 -127
- package/ftm-inbox/backend/planner/schema.py +34 -34
- package/ftm-inbox/backend/requirements.txt +5 -5
- package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -186
- package/ftm-inbox/backend/routes/health.py +52 -52
- package/ftm-inbox/backend/routes/inbox.py +68 -68
- package/ftm-inbox/backend/routes/plan.py +271 -271
- package/ftm-inbox/bin/launchagent.mjs +91 -91
- package/ftm-inbox/bin/setup.mjs +188 -188
- package/ftm-inbox/bin/start.sh +10 -10
- package/ftm-inbox/bin/status.sh +17 -17
- package/ftm-inbox/bin/stop.sh +8 -8
- package/ftm-inbox/config.example.yml +55 -55
- package/ftm-inbox/package-lock.json +2898 -2898
- package/ftm-inbox/package.json +26 -26
- package/ftm-inbox/postcss.config.js +6 -6
- package/ftm-inbox/src/app.css +199 -199
- package/ftm-inbox/src/app.html +18 -18
- package/ftm-inbox/src/lib/api.ts +166 -166
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
- package/ftm-inbox/src/lib/theme.ts +47 -47
- package/ftm-inbox/src/routes/+layout.svelte +76 -76
- package/ftm-inbox/src/routes/+page.svelte +401 -401
- package/ftm-inbox/svelte.config.js +12 -12
- package/ftm-inbox/tailwind.config.ts +63 -63
- package/ftm-inbox/tsconfig.json +13 -13
- package/ftm-inbox/vite.config.ts +6 -6
- package/ftm-intent/SKILL.md +241 -241
- package/ftm-intent.yml +2 -2
- package/ftm-manifest.json +3794 -3794
- package/ftm-map/SKILL.md +291 -291
- package/ftm-map/scripts/db.py +712 -712
- package/ftm-map/scripts/index.py +415 -415
- package/ftm-map/scripts/parser.py +224 -224
- package/ftm-map/scripts/queries/go-tags.scm +20 -20
- package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
- package/ftm-map/scripts/queries/python-tags.scm +31 -31
- package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
- package/ftm-map/scripts/queries/rust-tags.scm +37 -37
- package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
- package/ftm-map/scripts/query.py +301 -301
- package/ftm-map/scripts/ranker.py +377 -377
- package/ftm-map/scripts/requirements.txt +5 -5
- package/ftm-map/scripts/setup-hooks.sh +27 -27
- package/ftm-map/scripts/setup.sh +56 -56
- package/ftm-map/scripts/test_db.py +364 -364
- package/ftm-map/scripts/test_parser.py +174 -174
- package/ftm-map/scripts/test_query.py +183 -183
- package/ftm-map/scripts/test_ranker.py +199 -199
- package/ftm-map/scripts/views.py +591 -591
- package/ftm-map.yml +2 -2
- package/ftm-mind/SKILL.md +201 -1943
- package/ftm-mind/evals/promptfoo.yaml +142 -142
- package/ftm-mind/references/blackboard-protocol.md +110 -0
- package/ftm-mind/references/blackboard-schema.md +328 -328
- package/ftm-mind/references/complexity-guide.md +110 -110
- package/ftm-mind/references/complexity-sizing.md +138 -0
- package/ftm-mind/references/decide-act-protocol.md +172 -0
- package/ftm-mind/references/direct-execution.md +51 -0
- package/ftm-mind/references/environment-discovery.md +77 -0
- package/ftm-mind/references/event-registry.md +319 -319
- package/ftm-mind/references/mcp-inventory.md +300 -296
- package/ftm-mind/references/ops-routing.md +47 -0
- package/ftm-mind/references/orient-protocol.md +234 -0
- package/ftm-mind/references/personality.md +40 -0
- package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
- package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
- package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
- package/ftm-mind/references/reflexion-protocol.md +249 -249
- package/ftm-mind/references/routing/SCENARIOS.md +22 -22
- package/ftm-mind/references/routing-scenarios.md +35 -35
- package/ftm-mind.yml +2 -2
- package/ftm-ops.yml +4 -0
- package/ftm-pause/SKILL.md +395 -395
- package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
- package/ftm-pause/references/protocols/VALIDATION.md +80 -80
- package/ftm-pause.yml +2 -2
- package/ftm-researcher/SKILL.md +275 -275
- package/ftm-researcher/evals/agent-diversity.yaml +17 -17
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
- package/ftm-researcher/references/adaptive-search.md +116 -116
- package/ftm-researcher/references/agent-prompts.md +193 -193
- package/ftm-researcher/references/council-integration.md +193 -193
- package/ftm-researcher/references/output-format.md +203 -203
- package/ftm-researcher/references/synthesis-pipeline.md +165 -165
- package/ftm-researcher/scripts/score_credibility.py +234 -234
- package/ftm-researcher/scripts/validate_research.py +92 -92
- package/ftm-researcher.yml +2 -2
- package/ftm-resume/SKILL.md +518 -518
- package/ftm-resume/references/protocols/VALIDATION.md +172 -172
- package/ftm-resume.yml +2 -2
- package/ftm-retro/SKILL.md +380 -380
- package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
- package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
- package/ftm-retro.yml +2 -2
- package/ftm-routine/SKILL.md +170 -170
- package/ftm-routine.yml +4 -4
- package/ftm-state/blackboard/capabilities.json +5 -5
- package/ftm-state/blackboard/capabilities.schema.json +27 -27
- package/ftm-state/blackboard/context.json +37 -23
- package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
- package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
- package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
- package/ftm-state/blackboard/experiences/index.json +58 -9
- package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
- package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
- package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
- package/ftm-state/blackboard/patterns.json +6 -6
- package/ftm-state/schemas/context.schema.json +130 -130
- package/ftm-state/schemas/experience-index.schema.json +77 -77
- package/ftm-state/schemas/experience.schema.json +78 -78
- package/ftm-state/schemas/patterns.schema.json +44 -44
- package/ftm-upgrade/SKILL.md +194 -194
- package/ftm-upgrade/scripts/check-version.sh +76 -76
- package/ftm-upgrade/scripts/upgrade.sh +143 -143
- package/ftm-upgrade.yml +2 -2
- package/ftm-verify.yml +2 -2
- package/ftm.yml +2 -2
- package/hooks/ftm-auto-log.sh +137 -0
- package/hooks/ftm-blackboard-enforcer.sh +93 -93
- package/hooks/ftm-discovery-reminder.sh +90 -90
- package/hooks/ftm-drafts-gate.sh +61 -61
- package/hooks/ftm-event-logger.mjs +107 -107
- package/hooks/ftm-install-hooks.sh +240 -0
- package/hooks/ftm-learning-capture.sh +117 -0
- package/hooks/ftm-map-autodetect.sh +79 -79
- package/hooks/ftm-pending-sync-check.sh +22 -22
- package/hooks/ftm-plan-gate.sh +92 -92
- package/hooks/ftm-post-commit-trigger.sh +57 -57
- package/hooks/ftm-post-compaction.sh +138 -0
- package/hooks/ftm-pre-compaction.sh +147 -0
- package/hooks/ftm-session-end.sh +52 -0
- package/hooks/ftm-session-snapshot.sh +213 -0
- package/hooks/settings-template.json +81 -81
- package/install.sh +363 -363
- package/package.json +84 -84
- package/uninstall.sh +25 -25
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
14
|
+
HOME = Path.home()
|
|
15
|
+
CLAUDE_HOME = HOME / ".claude"
|
|
16
|
+
CLAUDE_SKILLS = CLAUDE_HOME / "skills"
|
|
17
|
+
DEFAULT_OUTPUT = REPO_ROOT / "codex-skills"
|
|
18
|
+
|
|
19
|
+
SKIP_DIRS = {
|
|
20
|
+
".claude",
|
|
21
|
+
".git",
|
|
22
|
+
".github",
|
|
23
|
+
".idea",
|
|
24
|
+
".pytest_cache",
|
|
25
|
+
".venv",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
"node_modules",
|
|
28
|
+
}
|
|
29
|
+
SKIP_FILES = {
|
|
30
|
+
".DS_Store",
|
|
31
|
+
}
|
|
32
|
+
SKIP_SUFFIXES = {
|
|
33
|
+
".pyc",
|
|
34
|
+
".pyo",
|
|
35
|
+
}
|
|
36
|
+
TEXT_SUFFIXES = {
|
|
37
|
+
"",
|
|
38
|
+
".css",
|
|
39
|
+
".html",
|
|
40
|
+
".js",
|
|
41
|
+
".json",
|
|
42
|
+
".md",
|
|
43
|
+
".mmd",
|
|
44
|
+
".py",
|
|
45
|
+
".rb",
|
|
46
|
+
".sh",
|
|
47
|
+
".sql",
|
|
48
|
+
".svg",
|
|
49
|
+
".toml",
|
|
50
|
+
".ts",
|
|
51
|
+
".tsx",
|
|
52
|
+
".txt",
|
|
53
|
+
".xml",
|
|
54
|
+
".yaml",
|
|
55
|
+
".yml",
|
|
56
|
+
}
|
|
57
|
+
KNOWN_ACRONYMS = {"api", "cli", "ftm", "html", "it", "json", "mcp", "okta", "scim", "sso", "ui", "yaml"}
|
|
58
|
+
COMMAND_SKILLS = [
|
|
59
|
+
"eng-buddy",
|
|
60
|
+
"ftm",
|
|
61
|
+
"ftm-audit",
|
|
62
|
+
"ftm-brainstorm",
|
|
63
|
+
"ftm-browse",
|
|
64
|
+
"ftm-capture",
|
|
65
|
+
"ftm-codex-gate",
|
|
66
|
+
"ftm-config",
|
|
67
|
+
"ftm-council",
|
|
68
|
+
"ftm-dashboard",
|
|
69
|
+
"ftm-debug",
|
|
70
|
+
"ftm-diagram",
|
|
71
|
+
"ftm-executor",
|
|
72
|
+
"ftm-git",
|
|
73
|
+
"ftm-intent",
|
|
74
|
+
"ftm-map",
|
|
75
|
+
"ftm-mind",
|
|
76
|
+
"ftm-pause",
|
|
77
|
+
"ftm-researcher",
|
|
78
|
+
"ftm-resume",
|
|
79
|
+
"ftm-retro",
|
|
80
|
+
"ftm-routine",
|
|
81
|
+
"ftm-upgrade",
|
|
82
|
+
"my-insights",
|
|
83
|
+
"skill-creator",
|
|
84
|
+
"sso-buddy",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class SkillSource:
|
|
90
|
+
name: str
|
|
91
|
+
skill_dir: Path
|
|
92
|
+
source_root: Path
|
|
93
|
+
sidecar: Path | None
|
|
94
|
+
preferred: bool
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def find_skill_markdown(skill_dir: Path) -> Path | None:
|
|
98
|
+
for candidate in (skill_dir / "SKILL.md", skill_dir / "Skill.md"):
|
|
99
|
+
if candidate.exists():
|
|
100
|
+
return candidate
|
|
101
|
+
for candidate in skill_dir.iterdir():
|
|
102
|
+
if candidate.is_file() and candidate.name.lower() == "skill.md":
|
|
103
|
+
return candidate
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_args() -> argparse.Namespace:
|
|
108
|
+
parser = argparse.ArgumentParser(
|
|
109
|
+
description="Convert Claude-oriented skill folders into Codex skill folders.",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--output",
|
|
113
|
+
type=Path,
|
|
114
|
+
default=DEFAULT_OUTPUT,
|
|
115
|
+
help=f"Output directory (default: {DEFAULT_OUTPUT})",
|
|
116
|
+
)
|
|
117
|
+
return parser.parse_args()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def discover_sources() -> tuple[list[SkillSource], list[str]]:
|
|
121
|
+
discovered: dict[str, SkillSource] = {}
|
|
122
|
+
|
|
123
|
+
for root, preferred in ((REPO_ROOT, True), (CLAUDE_SKILLS, False)):
|
|
124
|
+
if not root.exists():
|
|
125
|
+
continue
|
|
126
|
+
for skill_dir in sorted(path for path in root.iterdir() if path.is_dir()):
|
|
127
|
+
skill_md = find_skill_markdown(skill_dir)
|
|
128
|
+
if skill_md is None:
|
|
129
|
+
continue
|
|
130
|
+
name = skill_dir.name
|
|
131
|
+
sidecar = None
|
|
132
|
+
for suffix in (".yml", ".yaml"):
|
|
133
|
+
candidate = root / f"{name}{suffix}"
|
|
134
|
+
if candidate.exists():
|
|
135
|
+
sidecar = candidate
|
|
136
|
+
break
|
|
137
|
+
candidate = SkillSource(
|
|
138
|
+
name=name,
|
|
139
|
+
skill_dir=skill_dir,
|
|
140
|
+
source_root=root,
|
|
141
|
+
sidecar=sidecar,
|
|
142
|
+
preferred=preferred,
|
|
143
|
+
)
|
|
144
|
+
existing = discovered.get(name)
|
|
145
|
+
if existing is None or (preferred and not existing.preferred):
|
|
146
|
+
discovered[name] = candidate
|
|
147
|
+
|
|
148
|
+
skipped = []
|
|
149
|
+
if CLAUDE_SKILLS.exists():
|
|
150
|
+
yml_names = {p.stem for p in CLAUDE_SKILLS.glob("*.yml")} | {p.stem for p in CLAUDE_SKILLS.glob("*.yaml")}
|
|
151
|
+
skill_names = {p.parent.name for p in CLAUDE_SKILLS.glob("*/SKILL.md")}
|
|
152
|
+
skipped = sorted(yml_names - skill_names)
|
|
153
|
+
|
|
154
|
+
return sorted(discovered.values(), key=lambda item: item.name), skipped
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def should_ignore(path: Path) -> bool:
|
|
158
|
+
if path.name in SKIP_FILES:
|
|
159
|
+
return True
|
|
160
|
+
if path.suffix in SKIP_SUFFIXES:
|
|
161
|
+
return True
|
|
162
|
+
return any(part in SKIP_DIRS for part in path.parts)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def is_text_file(path: Path) -> bool:
|
|
166
|
+
if path.suffix.lower() in TEXT_SUFFIXES:
|
|
167
|
+
return True
|
|
168
|
+
if path.suffix:
|
|
169
|
+
return False
|
|
170
|
+
try:
|
|
171
|
+
path.read_text(encoding="utf-8")
|
|
172
|
+
return True
|
|
173
|
+
except UnicodeDecodeError:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def read_sidecar_metadata(sidecar: Path | None) -> dict[str, str]:
|
|
178
|
+
if sidecar is None or not sidecar.exists():
|
|
179
|
+
return {}
|
|
180
|
+
data: dict[str, str] = {}
|
|
181
|
+
for line in sidecar.read_text(encoding="utf-8").splitlines():
|
|
182
|
+
if ":" not in line:
|
|
183
|
+
continue
|
|
184
|
+
key, value = line.split(":", 1)
|
|
185
|
+
key = key.strip()
|
|
186
|
+
value = value.strip().strip('"').strip("'")
|
|
187
|
+
if key in {"name", "description"} and value:
|
|
188
|
+
data[key] = value
|
|
189
|
+
if data.keys() >= {"name", "description"}:
|
|
190
|
+
break
|
|
191
|
+
return data
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def split_frontmatter(text: str) -> tuple[dict[str, str], str]:
|
|
195
|
+
if not text.startswith("---\n"):
|
|
196
|
+
return {}, text
|
|
197
|
+
match = re.match(r"^---\n(.*?)\n---\n?", text, re.DOTALL)
|
|
198
|
+
if not match:
|
|
199
|
+
return {}, text
|
|
200
|
+
metadata_block = match.group(1)
|
|
201
|
+
metadata: dict[str, str] = {}
|
|
202
|
+
for line in metadata_block.splitlines():
|
|
203
|
+
if ":" not in line:
|
|
204
|
+
continue
|
|
205
|
+
key, value = line.split(":", 1)
|
|
206
|
+
key = key.strip()
|
|
207
|
+
value = value.strip().strip('"').strip("'")
|
|
208
|
+
if key in {"name", "description"} and value:
|
|
209
|
+
metadata[key] = value
|
|
210
|
+
return metadata, text[match.end() :]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def extract_metadata_section(body: str) -> tuple[dict[str, str], str]:
|
|
214
|
+
match = re.search(
|
|
215
|
+
r"^## Metadata\s*\n(?P<section>(?:- .+\n)+)",
|
|
216
|
+
body,
|
|
217
|
+
re.MULTILINE,
|
|
218
|
+
)
|
|
219
|
+
if not match:
|
|
220
|
+
return {}, body
|
|
221
|
+
|
|
222
|
+
metadata: dict[str, str] = {}
|
|
223
|
+
for line in match.group("section").splitlines():
|
|
224
|
+
line = line.strip()
|
|
225
|
+
pair = re.match(r"- \*\*(?P<key>[^*]+)\*\*:\s*(?P<value>.+)", line)
|
|
226
|
+
if not pair:
|
|
227
|
+
continue
|
|
228
|
+
key = pair.group("key").strip().lower()
|
|
229
|
+
value = pair.group("value").strip()
|
|
230
|
+
if key in {"name", "description", "invocation"}:
|
|
231
|
+
metadata[key] = value
|
|
232
|
+
|
|
233
|
+
new_body = body[: match.start()] + body[match.end() :]
|
|
234
|
+
new_body = re.sub(r"\n{3,}", "\n\n", new_body).lstrip()
|
|
235
|
+
return metadata, new_body
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def normalize_description(description: str, fallback_name: str) -> str:
|
|
239
|
+
description = " ".join(description.split()).strip()
|
|
240
|
+
description = description.replace("Claude Code", "Codex")
|
|
241
|
+
if not description:
|
|
242
|
+
description = f"Converted Codex skill for {fallback_name}."
|
|
243
|
+
return description
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def format_display_name(skill_name: str) -> str:
|
|
247
|
+
parts = []
|
|
248
|
+
for piece in skill_name.split("-"):
|
|
249
|
+
lower = piece.lower()
|
|
250
|
+
if lower in KNOWN_ACRONYMS:
|
|
251
|
+
parts.append(lower.upper())
|
|
252
|
+
else:
|
|
253
|
+
parts.append(piece.capitalize())
|
|
254
|
+
return " ".join(parts)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def build_short_description(description: str, display_name: str) -> str:
|
|
258
|
+
sentence = f"Help with {display_name} workflows"
|
|
259
|
+
if len(sentence) > 64:
|
|
260
|
+
sentence = f"{display_name} workflows"
|
|
261
|
+
return sentence
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def build_default_prompt(skill_name: str, description: str) -> str:
|
|
265
|
+
return f"Use ${skill_name} when you need help with its workflows."
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def rewrite_commands(text: str, skill_names: list[str]) -> str:
|
|
269
|
+
for skill_name in sorted(skill_names, key=len, reverse=True):
|
|
270
|
+
text = re.sub(
|
|
271
|
+
rf"(?<![A-Za-z0-9_-])/{re.escape(skill_name)}\b",
|
|
272
|
+
f"${skill_name}",
|
|
273
|
+
text,
|
|
274
|
+
)
|
|
275
|
+
return text
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def rewrite_paths(text: str) -> str:
|
|
279
|
+
replacements = [
|
|
280
|
+
("~/Documents/Code/kioja-scratch-paper/sso-plan.md", "$CODEX_HOME/skills/sso-buddy/sso-plan.md"),
|
|
281
|
+
(str(HOME / ".claude" / "skills") + "/", "$CODEX_HOME/skills/"),
|
|
282
|
+
(str(HOME / ".claude") + "/", "$CODEX_HOME/"),
|
|
283
|
+
("$HOME/.claude/skills/", "$CODEX_HOME/skills/"),
|
|
284
|
+
("$HOME/.claude/", "$CODEX_HOME/"),
|
|
285
|
+
("$CLAUDE_HOME/skills/", "$CODEX_HOME/skills/"),
|
|
286
|
+
("$CLAUDE_HOME/", "$CODEX_HOME/"),
|
|
287
|
+
("'.claude/skills/", "'.codex/skills/"),
|
|
288
|
+
('".claude/skills/', '".codex/skills/'),
|
|
289
|
+
("'.claude/", "'.codex/"),
|
|
290
|
+
('".claude/', '".codex/'),
|
|
291
|
+
("/.claude/skills/", "/.codex/skills/"),
|
|
292
|
+
("/.claude/", "/.codex/"),
|
|
293
|
+
("~/.claude/skills/", "$CODEX_HOME/skills/"),
|
|
294
|
+
("~/.claude/", "$CODEX_HOME/"),
|
|
295
|
+
]
|
|
296
|
+
for old, new in replacements:
|
|
297
|
+
text = text.replace(old, new)
|
|
298
|
+
return text
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def normalize_text(text: str, skill_names: list[str]) -> str:
|
|
302
|
+
text = rewrite_paths(text)
|
|
303
|
+
text = rewrite_commands(text, skill_names)
|
|
304
|
+
text = text.replace("Claude Code", "Codex")
|
|
305
|
+
text = text.replace("claude code", "codex")
|
|
306
|
+
return text
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def normalize_skill_markdown(source: SkillSource, text: str, skill_names: list[str]) -> str:
|
|
310
|
+
frontmatter, body = split_frontmatter(text)
|
|
311
|
+
inline_metadata, body = extract_metadata_section(body)
|
|
312
|
+
sidecar = read_sidecar_metadata(source.sidecar)
|
|
313
|
+
|
|
314
|
+
name = source.name
|
|
315
|
+
description = (
|
|
316
|
+
frontmatter.get("description")
|
|
317
|
+
or sidecar.get("description")
|
|
318
|
+
or inline_metadata.get("description")
|
|
319
|
+
or ""
|
|
320
|
+
)
|
|
321
|
+
description = normalize_description(description, name)
|
|
322
|
+
body = normalize_text(body.strip() + "\n", skill_names)
|
|
323
|
+
|
|
324
|
+
header = f"---\nname: {name}\ndescription: {description}\n---\n\n"
|
|
325
|
+
return header + body
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def normalize_generic_text(text: str, skill_names: list[str]) -> str:
|
|
329
|
+
return normalize_text(text, skill_names)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def write_openai_yaml(skill_dir: Path, skill_name: str, description: str) -> None:
|
|
333
|
+
display_name = format_display_name(skill_name)
|
|
334
|
+
short_description = build_short_description(description, display_name)
|
|
335
|
+
default_prompt = build_default_prompt(skill_name, description)
|
|
336
|
+
content = "\n".join(
|
|
337
|
+
[
|
|
338
|
+
"interface:",
|
|
339
|
+
f' display_name: "{display_name}"',
|
|
340
|
+
f' short_description: "{short_description}"',
|
|
341
|
+
f' default_prompt: "{default_prompt.replace(chr(34), chr(92) + chr(34))}"',
|
|
342
|
+
"",
|
|
343
|
+
"policy:",
|
|
344
|
+
" allow_implicit_invocation: true",
|
|
345
|
+
"",
|
|
346
|
+
]
|
|
347
|
+
)
|
|
348
|
+
agents_dir = skill_dir / "agents"
|
|
349
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
350
|
+
(agents_dir / "openai.yaml").write_text(content, encoding="utf-8")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def collect_warnings(skill_dir: Path) -> dict[str, int]:
|
|
354
|
+
warning_patterns = {
|
|
355
|
+
"claude_home_refs": re.compile(r"\.claude"),
|
|
356
|
+
"claude_cli_refs": re.compile(r"(?<![A-Za-z0-9_-])claude(?![A-Za-z0-9_-])"),
|
|
357
|
+
"anthropic_refs": re.compile(r"Anthropic"),
|
|
358
|
+
"absolute_home_refs": re.compile(re.escape(str(HOME))),
|
|
359
|
+
}
|
|
360
|
+
counts = {key: 0 for key in warning_patterns}
|
|
361
|
+
for path in skill_dir.rglob("*"):
|
|
362
|
+
if not path.is_file() or should_ignore(path) or not is_text_file(path):
|
|
363
|
+
continue
|
|
364
|
+
text = path.read_text(encoding="utf-8")
|
|
365
|
+
for key, pattern in warning_patterns.items():
|
|
366
|
+
counts[key] += len(pattern.findall(text))
|
|
367
|
+
return counts
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def copy_and_normalize(source: SkillSource, output_root: Path, skill_names: list[str]) -> dict[str, object]:
|
|
371
|
+
target_dir = output_root / source.name
|
|
372
|
+
if target_dir.exists():
|
|
373
|
+
shutil.rmtree(target_dir)
|
|
374
|
+
shutil.copytree(
|
|
375
|
+
source.skill_dir,
|
|
376
|
+
target_dir,
|
|
377
|
+
ignore=shutil.ignore_patterns(*SKIP_DIRS, *SKIP_FILES, "*.pyc", "*.pyo"),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
copied_skill_md = find_skill_markdown(target_dir)
|
|
381
|
+
if copied_skill_md is not None and copied_skill_md.name != "SKILL.md":
|
|
382
|
+
copied_skill_md.rename(target_dir / "SKILL.md")
|
|
383
|
+
|
|
384
|
+
for path in sorted(target_dir.rglob("*")):
|
|
385
|
+
if not path.is_file() or should_ignore(path) or not is_text_file(path):
|
|
386
|
+
continue
|
|
387
|
+
text = path.read_text(encoding="utf-8")
|
|
388
|
+
if path.name.lower() == "skill.md":
|
|
389
|
+
text = normalize_skill_markdown(source, text, skill_names)
|
|
390
|
+
else:
|
|
391
|
+
text = normalize_generic_text(text, skill_names)
|
|
392
|
+
path.write_text(text, encoding="utf-8")
|
|
393
|
+
|
|
394
|
+
skill_md = target_dir / "SKILL.md"
|
|
395
|
+
_, body = split_frontmatter(skill_md.read_text(encoding="utf-8"))
|
|
396
|
+
sidecar = read_sidecar_metadata(source.sidecar)
|
|
397
|
+
description = sidecar.get("description", "")
|
|
398
|
+
if not description:
|
|
399
|
+
match = re.search(r"^description:\s*(.+)$", skill_md.read_text(encoding="utf-8"), re.MULTILINE)
|
|
400
|
+
if match:
|
|
401
|
+
description = match.group(1).strip()
|
|
402
|
+
description = normalize_description(description, source.name)
|
|
403
|
+
write_openai_yaml(target_dir, source.name, description)
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
"name": source.name,
|
|
407
|
+
"source": str(source.skill_dir),
|
|
408
|
+
"output": str(target_dir),
|
|
409
|
+
"warnings": collect_warnings(target_dir),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def write_report(
|
|
414
|
+
output_root: Path,
|
|
415
|
+
converted: list[dict[str, object]],
|
|
416
|
+
skipped: list[str],
|
|
417
|
+
) -> None:
|
|
418
|
+
lines = [
|
|
419
|
+
"# Codex Skill Conversion Report",
|
|
420
|
+
"",
|
|
421
|
+
f"Converted {len(converted)} skills into `{output_root}`.",
|
|
422
|
+
"",
|
|
423
|
+
"## Converted Skills",
|
|
424
|
+
"",
|
|
425
|
+
]
|
|
426
|
+
for item in converted:
|
|
427
|
+
lines.append(f"- `{item['name']}` from `{item['source']}`")
|
|
428
|
+
|
|
429
|
+
if skipped:
|
|
430
|
+
lines.extend(
|
|
431
|
+
[
|
|
432
|
+
"",
|
|
433
|
+
"## Skipped Manifests",
|
|
434
|
+
"",
|
|
435
|
+
"These Claude manifest files did not have a matching skill directory with `SKILL.md`:",
|
|
436
|
+
"",
|
|
437
|
+
]
|
|
438
|
+
)
|
|
439
|
+
for name in skipped:
|
|
440
|
+
lines.append(f"- `{name}`")
|
|
441
|
+
|
|
442
|
+
lines.extend(
|
|
443
|
+
[
|
|
444
|
+
"",
|
|
445
|
+
"## Remaining Portability Warnings",
|
|
446
|
+
"",
|
|
447
|
+
"These counts show where Claude-specific assumptions still remain after the automated pass.",
|
|
448
|
+
"",
|
|
449
|
+
]
|
|
450
|
+
)
|
|
451
|
+
for item in converted:
|
|
452
|
+
warnings = item["warnings"]
|
|
453
|
+
if not any(warnings.values()):
|
|
454
|
+
continue
|
|
455
|
+
warning_summary = ", ".join(f"{key}={value}" for key, value in warnings.items() if value)
|
|
456
|
+
lines.append(f"- `{item['name']}`: {warning_summary}")
|
|
457
|
+
|
|
458
|
+
(output_root / "CONVERSION_REPORT.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
459
|
+
(output_root / "conversion-report.json").write_text(
|
|
460
|
+
json.dumps(
|
|
461
|
+
{
|
|
462
|
+
"converted": converted,
|
|
463
|
+
"skipped_manifests": skipped,
|
|
464
|
+
},
|
|
465
|
+
indent=2,
|
|
466
|
+
)
|
|
467
|
+
+ "\n",
|
|
468
|
+
encoding="utf-8",
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def main() -> None:
|
|
473
|
+
args = parse_args()
|
|
474
|
+
output_root = args.output.resolve()
|
|
475
|
+
sources, skipped = discover_sources()
|
|
476
|
+
skill_names = [source.name for source in sources if source.name in COMMAND_SKILLS or True]
|
|
477
|
+
|
|
478
|
+
if output_root.exists():
|
|
479
|
+
shutil.rmtree(output_root)
|
|
480
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
481
|
+
|
|
482
|
+
converted: list[dict[str, object]] = []
|
|
483
|
+
for source in sources:
|
|
484
|
+
converted.append(copy_and_normalize(source, output_root, skill_names))
|
|
485
|
+
|
|
486
|
+
write_report(output_root, converted, skipped)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
if __name__ == "__main__":
|
|
490
|
+
main()
|