feed-the-machine 1.6.1 → 1.7.1

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