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.
Files changed (269) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +170 -170
  3. package/bin/brain.py +1340 -0
  4. package/bin/convert_claude_skills_to_codex.py +490 -0
  5. package/bin/generate-manifest.mjs +463 -463
  6. package/bin/harden_codex_skills.py +141 -0
  7. package/bin/install.mjs +491 -491
  8. package/bin/migrate-eng-buddy-data.py +875 -0
  9. package/bin/playbook_engine/__init__.py +1 -0
  10. package/bin/playbook_engine/conftest.py +8 -0
  11. package/bin/playbook_engine/extractor.py +33 -0
  12. package/bin/playbook_engine/manager.py +102 -0
  13. package/bin/playbook_engine/models.py +84 -0
  14. package/bin/playbook_engine/registry.py +35 -0
  15. package/bin/playbook_engine/test_extractor.py +72 -0
  16. package/bin/playbook_engine/test_integration.py +129 -0
  17. package/bin/playbook_engine/test_manager.py +85 -0
  18. package/bin/playbook_engine/test_models.py +166 -0
  19. package/bin/playbook_engine/test_registry.py +67 -0
  20. package/bin/playbook_engine/test_tracer.py +86 -0
  21. package/bin/playbook_engine/tracer.py +93 -0
  22. package/bin/tasks_db.py +456 -0
  23. package/docs/HOOKS.md +243 -243
  24. package/docs/INBOX.md +233 -233
  25. package/ftm/SKILL.md +125 -122
  26. package/ftm-audit/SKILL.md +623 -623
  27. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
  28. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
  29. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
  30. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
  31. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
  32. package/ftm-audit/scripts/run-knip.sh +23 -23
  33. package/ftm-audit.yml +2 -2
  34. package/ftm-brainstorm/SKILL.md +1003 -498
  35. package/ftm-brainstorm/evals/evals.json +180 -100
  36. package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
  37. package/ftm-brainstorm/references/agent-prompts.md +552 -224
  38. package/ftm-brainstorm/references/plan-template.md +209 -121
  39. package/ftm-brainstorm.yml +2 -2
  40. package/ftm-browse/SKILL.md +454 -454
  41. package/ftm-browse/daemon/browser-manager.ts +206 -206
  42. package/ftm-browse/daemon/bun.lock +30 -30
  43. package/ftm-browse/daemon/cli.ts +347 -347
  44. package/ftm-browse/daemon/commands.ts +410 -410
  45. package/ftm-browse/daemon/main.ts +357 -357
  46. package/ftm-browse/daemon/package.json +17 -17
  47. package/ftm-browse/daemon/server.ts +189 -189
  48. package/ftm-browse/daemon/snapshot.ts +519 -519
  49. package/ftm-browse/daemon/tsconfig.json +22 -22
  50. package/ftm-browse.yml +4 -4
  51. package/ftm-capture/SKILL.md +370 -370
  52. package/ftm-capture.yml +4 -4
  53. package/ftm-codex-gate/SKILL.md +361 -361
  54. package/ftm-codex-gate.yml +2 -2
  55. package/ftm-config/SKILL.md +422 -345
  56. package/ftm-config.default.yml +125 -82
  57. package/ftm-config.yml +44 -2
  58. package/ftm-council/SKILL.md +416 -416
  59. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
  60. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
  61. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
  62. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
  63. package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
  64. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
  65. package/ftm-council.yml +2 -2
  66. package/ftm-dashboard/SKILL.md +163 -163
  67. package/ftm-dashboard.yml +4 -4
  68. package/ftm-debug/SKILL.md +1037 -1037
  69. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
  70. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
  71. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
  72. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
  73. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
  74. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
  75. package/ftm-debug.yml +2 -2
  76. package/ftm-diagram/SKILL.md +277 -277
  77. package/ftm-diagram.yml +2 -2
  78. package/ftm-executor/SKILL.md +777 -777
  79. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
  80. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
  81. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
  82. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
  83. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
  84. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
  85. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
  86. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
  87. package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
  88. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
  89. package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
  90. package/ftm-executor/runtime/package.json +8 -8
  91. package/ftm-executor.yml +2 -2
  92. package/ftm-git/SKILL.md +441 -441
  93. package/ftm-git/evals/evals.json +26 -26
  94. package/ftm-git/evals/promptfoo.yaml +75 -75
  95. package/ftm-git/hooks/post-commit-experience.sh +92 -92
  96. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
  97. package/ftm-git/references/protocols/REMEDIATION.md +139 -139
  98. package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
  99. package/ftm-git.yml +2 -2
  100. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  101. package/ftm-inbox/backend/adapters/_retry.py +64 -64
  102. package/ftm-inbox/backend/adapters/base.py +230 -230
  103. package/ftm-inbox/backend/adapters/freshservice.py +104 -104
  104. package/ftm-inbox/backend/adapters/gmail.py +125 -125
  105. package/ftm-inbox/backend/adapters/jira.py +136 -136
  106. package/ftm-inbox/backend/adapters/registry.py +192 -192
  107. package/ftm-inbox/backend/adapters/slack.py +110 -110
  108. package/ftm-inbox/backend/db/connection.py +54 -54
  109. package/ftm-inbox/backend/db/schema.py +78 -78
  110. package/ftm-inbox/backend/executor/__init__.py +7 -7
  111. package/ftm-inbox/backend/executor/engine.py +149 -149
  112. package/ftm-inbox/backend/executor/step_runner.py +98 -98
  113. package/ftm-inbox/backend/main.py +103 -103
  114. package/ftm-inbox/backend/models/__init__.py +1 -1
  115. package/ftm-inbox/backend/models/unified_task.py +36 -36
  116. package/ftm-inbox/backend/planner/__init__.py +6 -6
  117. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  118. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  119. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  120. package/ftm-inbox/backend/planner/generator.py +127 -127
  121. package/ftm-inbox/backend/planner/schema.py +34 -34
  122. package/ftm-inbox/backend/requirements.txt +5 -5
  123. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  124. package/ftm-inbox/backend/routes/execute.py +186 -186
  125. package/ftm-inbox/backend/routes/health.py +52 -52
  126. package/ftm-inbox/backend/routes/inbox.py +68 -68
  127. package/ftm-inbox/backend/routes/plan.py +271 -271
  128. package/ftm-inbox/bin/launchagent.mjs +91 -91
  129. package/ftm-inbox/bin/setup.mjs +188 -188
  130. package/ftm-inbox/bin/start.sh +10 -10
  131. package/ftm-inbox/bin/status.sh +17 -17
  132. package/ftm-inbox/bin/stop.sh +8 -8
  133. package/ftm-inbox/config.example.yml +55 -55
  134. package/ftm-inbox/package-lock.json +2898 -2898
  135. package/ftm-inbox/package.json +26 -26
  136. package/ftm-inbox/postcss.config.js +6 -6
  137. package/ftm-inbox/src/app.css +199 -199
  138. package/ftm-inbox/src/app.html +18 -18
  139. package/ftm-inbox/src/lib/api.ts +166 -166
  140. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
  141. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
  142. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
  143. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
  144. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
  145. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
  146. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
  147. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
  148. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
  149. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
  150. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
  151. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
  152. package/ftm-inbox/src/lib/theme.ts +47 -47
  153. package/ftm-inbox/src/routes/+layout.svelte +76 -76
  154. package/ftm-inbox/src/routes/+page.svelte +401 -401
  155. package/ftm-inbox/svelte.config.js +12 -12
  156. package/ftm-inbox/tailwind.config.ts +63 -63
  157. package/ftm-inbox/tsconfig.json +13 -13
  158. package/ftm-inbox/vite.config.ts +6 -6
  159. package/ftm-intent/SKILL.md +241 -241
  160. package/ftm-intent.yml +2 -2
  161. package/ftm-manifest.json +3794 -3794
  162. package/ftm-map/SKILL.md +291 -291
  163. package/ftm-map/scripts/db.py +712 -712
  164. package/ftm-map/scripts/index.py +415 -415
  165. package/ftm-map/scripts/parser.py +224 -224
  166. package/ftm-map/scripts/queries/go-tags.scm +20 -20
  167. package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
  168. package/ftm-map/scripts/queries/python-tags.scm +31 -31
  169. package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
  170. package/ftm-map/scripts/queries/rust-tags.scm +37 -37
  171. package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
  172. package/ftm-map/scripts/query.py +301 -301
  173. package/ftm-map/scripts/ranker.py +377 -377
  174. package/ftm-map/scripts/requirements.txt +5 -5
  175. package/ftm-map/scripts/setup-hooks.sh +27 -27
  176. package/ftm-map/scripts/setup.sh +56 -56
  177. package/ftm-map/scripts/test_db.py +364 -364
  178. package/ftm-map/scripts/test_parser.py +174 -174
  179. package/ftm-map/scripts/test_query.py +183 -183
  180. package/ftm-map/scripts/test_ranker.py +199 -199
  181. package/ftm-map/scripts/views.py +591 -591
  182. package/ftm-map.yml +2 -2
  183. package/ftm-mind/SKILL.md +201 -1943
  184. package/ftm-mind/evals/promptfoo.yaml +142 -142
  185. package/ftm-mind/references/blackboard-protocol.md +110 -0
  186. package/ftm-mind/references/blackboard-schema.md +328 -328
  187. package/ftm-mind/references/complexity-guide.md +110 -110
  188. package/ftm-mind/references/complexity-sizing.md +138 -0
  189. package/ftm-mind/references/decide-act-protocol.md +172 -0
  190. package/ftm-mind/references/direct-execution.md +51 -0
  191. package/ftm-mind/references/environment-discovery.md +77 -0
  192. package/ftm-mind/references/event-registry.md +319 -319
  193. package/ftm-mind/references/mcp-inventory.md +300 -296
  194. package/ftm-mind/references/ops-routing.md +47 -0
  195. package/ftm-mind/references/orient-protocol.md +234 -0
  196. package/ftm-mind/references/personality.md +40 -0
  197. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
  198. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
  199. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
  200. package/ftm-mind/references/reflexion-protocol.md +249 -249
  201. package/ftm-mind/references/routing/SCENARIOS.md +22 -22
  202. package/ftm-mind/references/routing-scenarios.md +35 -35
  203. package/ftm-mind.yml +2 -2
  204. package/ftm-ops.yml +4 -0
  205. package/ftm-pause/SKILL.md +395 -395
  206. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
  207. package/ftm-pause/references/protocols/VALIDATION.md +80 -80
  208. package/ftm-pause.yml +2 -2
  209. package/ftm-researcher/SKILL.md +275 -275
  210. package/ftm-researcher/evals/agent-diversity.yaml +17 -17
  211. package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
  212. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
  213. package/ftm-researcher/references/adaptive-search.md +116 -116
  214. package/ftm-researcher/references/agent-prompts.md +193 -193
  215. package/ftm-researcher/references/council-integration.md +193 -193
  216. package/ftm-researcher/references/output-format.md +203 -203
  217. package/ftm-researcher/references/synthesis-pipeline.md +165 -165
  218. package/ftm-researcher/scripts/score_credibility.py +234 -234
  219. package/ftm-researcher/scripts/validate_research.py +92 -92
  220. package/ftm-researcher.yml +2 -2
  221. package/ftm-resume/SKILL.md +518 -518
  222. package/ftm-resume/references/protocols/VALIDATION.md +172 -172
  223. package/ftm-resume.yml +2 -2
  224. package/ftm-retro/SKILL.md +380 -380
  225. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
  226. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
  227. package/ftm-retro.yml +2 -2
  228. package/ftm-routine/SKILL.md +170 -170
  229. package/ftm-routine.yml +4 -4
  230. package/ftm-state/blackboard/capabilities.json +5 -5
  231. package/ftm-state/blackboard/capabilities.schema.json +27 -27
  232. package/ftm-state/blackboard/context.json +37 -23
  233. package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
  234. package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
  235. package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
  236. package/ftm-state/blackboard/experiences/index.json +58 -9
  237. package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
  238. package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
  239. package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
  240. package/ftm-state/blackboard/patterns.json +6 -6
  241. package/ftm-state/schemas/context.schema.json +130 -130
  242. package/ftm-state/schemas/experience-index.schema.json +77 -77
  243. package/ftm-state/schemas/experience.schema.json +78 -78
  244. package/ftm-state/schemas/patterns.schema.json +44 -44
  245. package/ftm-upgrade/SKILL.md +194 -194
  246. package/ftm-upgrade/scripts/check-version.sh +76 -76
  247. package/ftm-upgrade/scripts/upgrade.sh +143 -143
  248. package/ftm-upgrade.yml +2 -2
  249. package/ftm-verify.yml +2 -2
  250. package/ftm.yml +2 -2
  251. package/hooks/ftm-auto-log.sh +137 -0
  252. package/hooks/ftm-blackboard-enforcer.sh +93 -93
  253. package/hooks/ftm-discovery-reminder.sh +90 -90
  254. package/hooks/ftm-drafts-gate.sh +61 -61
  255. package/hooks/ftm-event-logger.mjs +107 -107
  256. package/hooks/ftm-install-hooks.sh +240 -0
  257. package/hooks/ftm-learning-capture.sh +117 -0
  258. package/hooks/ftm-map-autodetect.sh +79 -79
  259. package/hooks/ftm-pending-sync-check.sh +22 -22
  260. package/hooks/ftm-plan-gate.sh +92 -92
  261. package/hooks/ftm-post-commit-trigger.sh +57 -57
  262. package/hooks/ftm-post-compaction.sh +138 -0
  263. package/hooks/ftm-pre-compaction.sh +147 -0
  264. package/hooks/ftm-session-end.sh +52 -0
  265. package/hooks/ftm-session-snapshot.sh +213 -0
  266. package/hooks/settings-template.json +81 -81
  267. package/install.sh +363 -363
  268. package/package.json +84 -84
  269. 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()