@softspark/ai-toolkit 4.14.0 → 4.15.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/CHANGELOG.md +35 -0
- package/README.md +11 -10
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/CLAUDE.md.template +3 -0
- package/app/agents/fact-checker.md +1 -1
- package/app/hooks/_search-capability.sh +3 -2
- package/app/hooks/stop-search-check.sh +2 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +73 -31
- package/kb/procedures/maintenance-sop.md +26 -13
- package/kb/procedures/release-verification-sop.md +41 -36
- package/kb/reference/architecture-overview.md +23 -7
- package/kb/reference/codex-cli-compatibility.md +96 -36
- package/kb/reference/extension-api.md +52 -9
- package/kb/reference/global-install-model.md +53 -21
- package/kb/reference/hooks-catalog.md +44 -8
- package/kb/reference/mcp-editor-compatibility.md +27 -6
- package/kb/reference/mcp-templates.md +12 -6
- package/kb/reference/opencode-compatibility.md +13 -7
- package/kb/reference/plugin-pack-conventions.md +7 -7
- package/kb/reference/skills-catalog.md +3 -3
- package/kb/reference/supported-tools-registry.md +19 -17
- package/kb/reference/windows-support.md +26 -3
- package/llms-full.txt +443 -180
- package/llms.txt +1 -1
- package/manifest.json +1 -1
- package/package.json +2 -2
- package/scripts/codex_skill_adapter.py +448 -198
- package/scripts/dir_rules_shared.py +2 -11
- package/scripts/ecosystem_tools.json +29 -8
- package/scripts/emission.py +5 -91
- package/scripts/generate_agents_md.py +4 -87
- package/scripts/generate_codex.py +5 -95
- package/scripts/generate_codex_agents.py +242 -0
- package/scripts/generate_codex_hooks.py +648 -55
- package/scripts/generate_codex_skills.py +15 -6
- package/scripts/generate_copilot.py +771 -74
- package/scripts/generate_copilot_hooks.py +606 -0
- package/scripts/generate_cursor_hooks.py +453 -121
- package/scripts/generate_opencode_commands.py +4 -6
- package/scripts/inject_hook_cli.py +770 -205
- package/scripts/injection.py +102 -23
- package/scripts/install_steps/ai_tools.py +123 -83
- package/scripts/instruction_core.py +95 -0
- package/scripts/mcp_editors.py +934 -80
- package/scripts/mcp_manager.py +46 -26
- package/scripts/plugin.py +291 -114
- package/scripts/secure_fs.py +538 -0
- package/scripts/uninstall.py +1279 -208
|
@@ -7,9 +7,12 @@ Codex subagents and plan tracking.
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import errno
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
10
13
|
import re
|
|
11
|
-
import shutil
|
|
12
14
|
import sys
|
|
15
|
+
import tempfile
|
|
13
16
|
from pathlib import Path
|
|
14
17
|
|
|
15
18
|
try:
|
|
@@ -25,45 +28,91 @@ CLAUDE_ONLY_TOOLS = frozenset({
|
|
|
25
28
|
"Skill", "EnterPlanMode", "ExitPlanMode",
|
|
26
29
|
})
|
|
27
30
|
|
|
28
|
-
CODEX_DELEGATION_TOOLS = (
|
|
29
|
-
"spawn_agent", "send_input", "wait_agent", "close_agent", "update_plan",
|
|
30
|
-
)
|
|
31
|
-
|
|
32
31
|
ADAPTED_MARKER = ".ai-toolkit-codex-adapted"
|
|
33
32
|
|
|
34
33
|
_FRONTMATTER_RE = re.compile(r"\A---\n(?P<frontmatter>.*?)\n---\n?(?P<body>.*)\Z", re.S)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
34
|
+
_AGENT_START_RE = re.compile(r"\bAgent\s*\(")
|
|
35
|
+
_TASK_API_RE = re.compile(r"\bTask(?:Create|List|Update|Get|Output|Stop)\b")
|
|
36
|
+
_POSITIONAL_ARGUMENT_RE = re.compile(r"\$([1-9])\b")
|
|
37
|
+
_PLATFORM_LABELS = {"codex": "Codex", "opencode": "OpenCode"}
|
|
38
|
+
_UNSUPPORTED_DIRECTORY_FSYNC_ERRNOS = frozenset({
|
|
39
|
+
errno.EBADF,
|
|
40
|
+
errno.EINVAL,
|
|
41
|
+
getattr(errno, "ENOTSUP", errno.EINVAL),
|
|
42
|
+
getattr(errno, "EOPNOTSUPP", errno.EINVAL),
|
|
43
|
+
})
|
|
44
|
+
_ADAPTATION_BODY_TOKENS = frozenset({
|
|
45
|
+
"$ARGUMENTS",
|
|
46
|
+
"CLAUDE_SKILL_DIR",
|
|
47
|
+
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
|
|
48
|
+
"spawn_agent",
|
|
49
|
+
"send_input",
|
|
50
|
+
"wait_agent",
|
|
51
|
+
"close_agent",
|
|
52
|
+
"update_plan",
|
|
53
|
+
"fork_context",
|
|
54
|
+
"agent_type=",
|
|
55
|
+
"TeamCreate",
|
|
56
|
+
"TeamDelete",
|
|
57
|
+
"SendMessage",
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _translation_note(platform: str) -> str:
|
|
62
|
+
label = _PLATFORM_LABELS[platform]
|
|
63
|
+
return f"""
|
|
64
|
+
## {label} Translation Layer
|
|
65
|
+
|
|
66
|
+
This generated {label} variant preserves the workflow intent using durable,
|
|
67
|
+
client-independent guidance:
|
|
68
|
+
|
|
69
|
+
- Use {label}-native subagents to delegate independent work when parallelism
|
|
70
|
+
materially improves speed or quality.
|
|
71
|
+
- Give each delegated task a narrow objective, relevant context, explicit file
|
|
72
|
+
ownership, and a clear expected result.
|
|
73
|
+
- Use the subagent controls available in the current client to steer or stop
|
|
74
|
+
delegated work without assuming a particular tool signature.
|
|
75
|
+
- Wait for delegated results only when the next critical-path step depends on
|
|
76
|
+
them, then integrate the results in the parent task.
|
|
77
|
+
- Track progress using the planning mechanism available in the current client
|
|
78
|
+
or an explicit local checklist.
|
|
79
|
+
- Treat a team as coordinated {label}-native subagents with non-overlapping work.
|
|
80
|
+
- Resolve `./` paths in command examples from the installed skill directory
|
|
81
|
+
that contains this `SKILL.md` file.
|
|
64
82
|
"""
|
|
65
83
|
|
|
66
84
|
|
|
85
|
+
def _semantic_replacements(platform: str) -> dict[str, str]:
|
|
86
|
+
label = _PLATFORM_LABELS[platform]
|
|
87
|
+
native_subagent = f"{label}-native subagent"
|
|
88
|
+
replacements = {
|
|
89
|
+
"${CLAUDE_SKILL_DIR}/": "./",
|
|
90
|
+
"$CLAUDE_SKILL_DIR/": "./",
|
|
91
|
+
"${CLAUDE_SKILL_DIR}": "the installed skill directory",
|
|
92
|
+
"CLAUDE_SKILL_DIR": "the installed skill directory",
|
|
93
|
+
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": f"{label} subagent support",
|
|
94
|
+
"spawn_agent": f"delegate independent work to a {native_subagent}",
|
|
95
|
+
"send_input": "steer a running subagent",
|
|
96
|
+
"wait_agent": "wait for delegated results",
|
|
97
|
+
"close_agent": "stop or finish delegated work",
|
|
98
|
+
"update_plan": "the planning mechanism available in the current client",
|
|
99
|
+
"fork_context": "appropriate inherited task context",
|
|
100
|
+
"agent_type=": "a suitable subagent role",
|
|
101
|
+
"TeamCreate": f"coordinate {label}-native subagents",
|
|
102
|
+
"TeamDelete": "finish coordinated subagent work",
|
|
103
|
+
"SendMessage": "steer a running subagent",
|
|
104
|
+
"TaskCreate": "the available planning mechanism",
|
|
105
|
+
"TaskList": "the available planning mechanism",
|
|
106
|
+
"TaskUpdate": "the available planning mechanism",
|
|
107
|
+
"TaskGet": "review delegated progress",
|
|
108
|
+
"TaskOutput": "collect delegated results",
|
|
109
|
+
"TaskStop": "stop delegated work",
|
|
110
|
+
}
|
|
111
|
+
if platform == "codex":
|
|
112
|
+
replacements["$ARGUMENTS"] = "the user-supplied task details"
|
|
113
|
+
return replacements
|
|
114
|
+
|
|
115
|
+
|
|
67
116
|
def skill_tools(skill_file: Path) -> list[str]:
|
|
68
117
|
"""Return ordered allowed-tools entries from a skill frontmatter block."""
|
|
69
118
|
tools_str = frontmatter_field(skill_file, "allowed-tools") or ""
|
|
@@ -71,8 +120,16 @@ def skill_tools(skill_file: Path) -> list[str]:
|
|
|
71
120
|
|
|
72
121
|
|
|
73
122
|
def is_codex_adapted_skill(skill_file: Path) -> bool:
|
|
74
|
-
"""Return True
|
|
75
|
-
|
|
123
|
+
"""Return True when a source skill needs portable client adaptation."""
|
|
124
|
+
if set(skill_tools(skill_file)) & CLAUDE_ONLY_TOOLS:
|
|
125
|
+
return True
|
|
126
|
+
text = skill_file.read_text(encoding="utf-8")
|
|
127
|
+
if any(token in text for token in _ADAPTATION_BODY_TOKENS):
|
|
128
|
+
return True
|
|
129
|
+
if _AGENT_START_RE.search(text) or _TASK_API_RE.search(text):
|
|
130
|
+
return True
|
|
131
|
+
is_user_invocable = frontmatter_field(skill_file, "user-invocable") != "false"
|
|
132
|
+
return is_user_invocable and bool(_POSITIONAL_ARGUMENT_RE.search(text))
|
|
76
133
|
|
|
77
134
|
|
|
78
135
|
def codex_skill_description(skill_file: Path) -> str:
|
|
@@ -81,95 +138,343 @@ def codex_skill_description(skill_file: Path) -> str:
|
|
|
81
138
|
if not description:
|
|
82
139
|
return ""
|
|
83
140
|
if is_codex_adapted_skill(skill_file):
|
|
84
|
-
return
|
|
141
|
+
return (
|
|
142
|
+
f"{description} Codex-adapted: uses Codex-native subagents and "
|
|
143
|
+
"current-client planning controls."
|
|
144
|
+
)
|
|
85
145
|
return description
|
|
86
146
|
|
|
87
147
|
|
|
88
148
|
def build_codex_skill_text(skill_file: Path) -> str:
|
|
89
149
|
"""Render the Codex-facing SKILL.md contents for a source skill."""
|
|
150
|
+
return _build_portable_skill_text(skill_file, "codex")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def build_opencode_skill_text(skill_file: Path) -> str:
|
|
154
|
+
"""Render an OpenCode-facing skill without leaking Codex branding."""
|
|
155
|
+
return _build_portable_skill_text(skill_file, "opencode")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _build_portable_skill_text(skill_file: Path, platform: str) -> str:
|
|
159
|
+
"""Render a client-specific skill using semantic, signature-free guidance."""
|
|
160
|
+
if platform not in _PLATFORM_LABELS:
|
|
161
|
+
raise ValueError(f"Unsupported skill adaptation platform: {platform}")
|
|
90
162
|
text = skill_file.read_text(encoding="utf-8")
|
|
91
163
|
match = _FRONTMATTER_RE.match(text)
|
|
92
164
|
if not match:
|
|
93
|
-
return text
|
|
165
|
+
return _adapt_body(text, platform) if is_codex_adapted_skill(skill_file) else text
|
|
94
166
|
|
|
95
|
-
frontmatter = _parse_frontmatter(match.group("frontmatter"))
|
|
96
167
|
body = match.group("body")
|
|
97
168
|
adapted = is_codex_adapted_skill(skill_file)
|
|
98
169
|
|
|
99
170
|
if adapted:
|
|
100
|
-
|
|
101
|
-
|
|
171
|
+
body = _adapt_body(body, platform)
|
|
172
|
+
name = frontmatter_field(skill_file, "name")
|
|
173
|
+
description = (
|
|
174
|
+
codex_skill_description(skill_file)
|
|
175
|
+
if platform == "codex"
|
|
176
|
+
else frontmatter_field(skill_file, "description")
|
|
177
|
+
)
|
|
178
|
+
rendered_frontmatter = "\n".join(
|
|
179
|
+
(f"name: {name}", f"description: {json.dumps(description, ensure_ascii=False)}")
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
frontmatter = _parse_frontmatter(match.group("frontmatter"))
|
|
183
|
+
rendered_frontmatter = _render_frontmatter(frontmatter)
|
|
102
184
|
|
|
103
|
-
rendered_frontmatter = _render_frontmatter(frontmatter)
|
|
104
185
|
return f"---\n{rendered_frontmatter}\n---\n{body.rstrip()}\n"
|
|
105
186
|
|
|
106
187
|
|
|
107
188
|
def sync_codex_skill(skill_dir: Path, skills_dst: Path) -> str:
|
|
108
189
|
"""Install one skill into `.agents/skills/` and return its mode."""
|
|
109
190
|
skill_file = skill_dir / "SKILL.md"
|
|
110
|
-
target = skills_dst / skill_dir.name
|
|
111
191
|
adapted = is_codex_adapted_skill(skill_file)
|
|
112
|
-
marker = target / ADAPTED_MARKER
|
|
113
192
|
|
|
114
193
|
if adapted:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
194
|
+
return _sync_adapted_skill(skill_dir, skills_dst)
|
|
195
|
+
return _sync_native_skill(skill_dir, skills_dst)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def prepare_codex_skills_dir(target_dir: Path) -> Path:
|
|
199
|
+
"""Create the documented Codex skill root without following symlinks."""
|
|
200
|
+
if target_dir.is_symlink():
|
|
201
|
+
raise RuntimeError(f"Refusing symlinked Codex target directory: {target_dir}")
|
|
202
|
+
agents_dir = target_dir / ".agents"
|
|
203
|
+
skills_dst = agents_dir / "skills"
|
|
204
|
+
_assert_safe_skill_roots(agents_dir, skills_dst)
|
|
205
|
+
skills_dst.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
_assert_safe_skill_roots(agents_dir, skills_dst)
|
|
207
|
+
return skills_dst
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def unmanaged_codex_skill_names(skills_dst: Path, skills_src: Path) -> set[str]:
|
|
211
|
+
"""Return logical names declared by user-owned destination entries."""
|
|
212
|
+
names: set[str] = set()
|
|
213
|
+
for item in sorted(skills_dst.iterdir()):
|
|
214
|
+
if _is_managed_entry(item, skills_src):
|
|
215
|
+
continue
|
|
216
|
+
skill_file = item / "SKILL.md"
|
|
217
|
+
try:
|
|
218
|
+
name = frontmatter_field(skill_file, "name")
|
|
219
|
+
except (OSError, UnicodeError):
|
|
220
|
+
continue
|
|
221
|
+
if name:
|
|
222
|
+
names.add(name)
|
|
223
|
+
return names
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def cleanup_codex_skills(
|
|
227
|
+
skills_dst: Path,
|
|
228
|
+
skills_src: Path,
|
|
229
|
+
blocked_names: set[str] | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Remove stale or shadowed toolkit-managed entries without touching user data."""
|
|
232
|
+
_assert_safe_skill_roots(skills_dst.parent, skills_dst)
|
|
233
|
+
skills_src_resolved = skills_src.resolve()
|
|
234
|
+
blocked_names = blocked_names or set()
|
|
235
|
+
for item in skills_dst.iterdir():
|
|
236
|
+
src = skills_src / item.name
|
|
237
|
+
if item.name in blocked_names and _is_managed_entry(item, skills_src):
|
|
238
|
+
if item.is_symlink():
|
|
239
|
+
item.unlink()
|
|
240
|
+
else:
|
|
241
|
+
_remove_managed_wrapper(item, skills_src_resolved)
|
|
242
|
+
continue
|
|
243
|
+
if item.is_symlink():
|
|
244
|
+
target = _symlink_target(item)
|
|
245
|
+
if src.is_dir() and target == src.resolve():
|
|
246
|
+
continue
|
|
247
|
+
if _is_relative_to(target, skills_src_resolved):
|
|
248
|
+
item.unlink()
|
|
249
|
+
continue
|
|
250
|
+
if _is_adapted_wrapper(item) and not src.is_dir():
|
|
251
|
+
_remove_managed_wrapper(item, skills_src_resolved)
|
|
123
252
|
|
|
124
|
-
target.mkdir(parents=True, exist_ok=True)
|
|
125
|
-
marker.write_text("generated by ai-toolkit for Codex\n", encoding="utf-8")
|
|
126
253
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
254
|
+
def _assert_safe_skill_roots(agents_dir: Path, skills_dst: Path) -> None:
|
|
255
|
+
if agents_dir.is_symlink():
|
|
256
|
+
raise RuntimeError(f"Refusing symlinked Codex agents directory: {agents_dir}")
|
|
257
|
+
if skills_dst.is_symlink():
|
|
258
|
+
raise RuntimeError(f"Refusing symlinked Codex skills directory: {skills_dst}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _symlink_target(path: Path) -> Path:
|
|
262
|
+
raw_target = Path(os.readlink(path))
|
|
263
|
+
if not raw_target.is_absolute():
|
|
264
|
+
raw_target = path.parent / raw_target
|
|
265
|
+
return raw_target.resolve(strict=False)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _points_to(path: Path, target: Path) -> bool:
|
|
269
|
+
return path.is_symlink() and _symlink_target(path) == target.resolve()
|
|
136
270
|
|
|
137
|
-
(target / "SKILL.md").write_text(build_codex_skill_text(skill_file), encoding="utf-8")
|
|
138
|
-
return "adapted"
|
|
139
271
|
|
|
272
|
+
def _is_adapted_wrapper(path: Path) -> bool:
|
|
273
|
+
if path.is_symlink() or not path.is_dir():
|
|
274
|
+
return False
|
|
275
|
+
marker = path / ADAPTED_MARKER
|
|
276
|
+
return not marker.is_symlink() and marker.is_file()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _is_managed_entry(path: Path, skills_src: Path) -> bool:
|
|
280
|
+
if _is_adapted_wrapper(path):
|
|
281
|
+
return True
|
|
282
|
+
if not path.is_symlink():
|
|
283
|
+
return False
|
|
284
|
+
return _is_relative_to(_symlink_target(path), skills_src.resolve())
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _sync_native_skill(skill_dir: Path, skills_dst: Path) -> str:
|
|
288
|
+
target = skills_dst / skill_dir.name
|
|
140
289
|
if target.is_symlink():
|
|
141
|
-
if target
|
|
142
|
-
|
|
143
|
-
target.
|
|
144
|
-
elif target.is_dir():
|
|
145
|
-
if marker.is_file():
|
|
146
|
-
shutil.rmtree(target)
|
|
147
|
-
else:
|
|
290
|
+
return "linked" if _points_to(target, skill_dir) else "skipped"
|
|
291
|
+
if _is_adapted_wrapper(target):
|
|
292
|
+
if not _remove_managed_wrapper(target, skill_dir.parent.resolve()):
|
|
148
293
|
return "skipped"
|
|
149
294
|
elif target.exists():
|
|
150
295
|
return "skipped"
|
|
151
|
-
|
|
152
296
|
target.symlink_to(skill_dir)
|
|
153
297
|
return "linked"
|
|
154
298
|
|
|
155
299
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
300
|
+
def _sync_adapted_skill(skill_dir: Path, skills_dst: Path) -> str:
|
|
301
|
+
target = skills_dst / skill_dir.name
|
|
302
|
+
if target.is_symlink():
|
|
303
|
+
if not _points_to(target, skill_dir):
|
|
304
|
+
return "skipped"
|
|
305
|
+
return _create_adapted_wrapper(skill_dir, target, replace_managed_link=True)
|
|
306
|
+
if target.exists() and not _is_adapted_wrapper(target):
|
|
307
|
+
return "skipped"
|
|
308
|
+
if not target.exists():
|
|
309
|
+
return _create_adapted_wrapper(skill_dir, target)
|
|
310
|
+
|
|
311
|
+
return _update_adapted_wrapper(skill_dir, target)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _create_adapted_wrapper(
|
|
315
|
+
skill_dir: Path,
|
|
316
|
+
target: Path,
|
|
317
|
+
*,
|
|
318
|
+
replace_managed_link: bool = False,
|
|
319
|
+
) -> str:
|
|
320
|
+
"""Build a complete sibling wrapper and expose it with one atomic rename."""
|
|
321
|
+
staging = Path(tempfile.mkdtemp(
|
|
322
|
+
dir=target.parent,
|
|
323
|
+
prefix=f".{target.name}.",
|
|
324
|
+
suffix=".tmp",
|
|
325
|
+
))
|
|
326
|
+
removed_link = False
|
|
327
|
+
try:
|
|
328
|
+
_write_text_fsync(
|
|
329
|
+
staging / "SKILL.md",
|
|
330
|
+
build_codex_skill_text(skill_dir / "SKILL.md"),
|
|
331
|
+
)
|
|
332
|
+
_write_text_fsync(
|
|
333
|
+
staging / ADAPTED_MARKER,
|
|
334
|
+
"generated by ai-toolkit for Codex\n",
|
|
335
|
+
)
|
|
336
|
+
_sync_auxiliaries(skill_dir, staging)
|
|
337
|
+
_fsync_directory(staging)
|
|
338
|
+
|
|
339
|
+
if replace_managed_link:
|
|
340
|
+
if not _points_to(target, skill_dir):
|
|
341
|
+
raise RuntimeError(f"Codex skill target changed during sync: {target}")
|
|
342
|
+
target.unlink()
|
|
343
|
+
removed_link = True
|
|
344
|
+
elif target.exists() or target.is_symlink():
|
|
345
|
+
raise RuntimeError(f"Codex skill target appeared during sync: {target}")
|
|
346
|
+
|
|
347
|
+
os.replace(staging, target)
|
|
348
|
+
_fsync_directory(target.parent)
|
|
349
|
+
return "adapted"
|
|
350
|
+
except Exception:
|
|
351
|
+
_remove_staged_wrapper(staging)
|
|
352
|
+
if removed_link and not target.exists() and not target.is_symlink():
|
|
353
|
+
target.symlink_to(skill_dir)
|
|
354
|
+
raise
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _update_adapted_wrapper(skill_dir: Path, target: Path) -> str:
|
|
358
|
+
"""Atomically refresh SKILL.md while leaving the managed marker stable."""
|
|
359
|
+
skill_output = target / "SKILL.md"
|
|
360
|
+
marker = target / ADAPTED_MARKER
|
|
361
|
+
if skill_output.is_symlink() or marker.is_symlink():
|
|
362
|
+
return "skipped"
|
|
363
|
+
temp_path = _stage_text(
|
|
364
|
+
skill_output,
|
|
365
|
+
build_codex_skill_text(skill_dir / "SKILL.md"),
|
|
366
|
+
)
|
|
367
|
+
try:
|
|
368
|
+
os.replace(temp_path, skill_output)
|
|
369
|
+
_fsync_directory(target)
|
|
370
|
+
finally:
|
|
371
|
+
temp_path.unlink(missing_ok=True)
|
|
372
|
+
_sync_auxiliaries(skill_dir, target)
|
|
373
|
+
return "adapted"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _write_text_fsync(destination: Path, content: str) -> None:
|
|
377
|
+
with destination.open("x", encoding="utf-8") as handle:
|
|
378
|
+
handle.write(content)
|
|
379
|
+
handle.flush()
|
|
380
|
+
os.fsync(handle.fileno())
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _fsync_directory(path: Path) -> None:
|
|
384
|
+
if os.name == "nt":
|
|
385
|
+
return
|
|
386
|
+
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
|
387
|
+
try:
|
|
388
|
+
fd = os.open(path, flags)
|
|
389
|
+
except OSError as error:
|
|
390
|
+
if error.errno in _UNSUPPORTED_DIRECTORY_FSYNC_ERRNOS:
|
|
391
|
+
return
|
|
392
|
+
raise
|
|
393
|
+
try:
|
|
394
|
+
try:
|
|
395
|
+
os.fsync(fd)
|
|
396
|
+
except OSError as error:
|
|
397
|
+
if error.errno not in _UNSUPPORTED_DIRECTORY_FSYNC_ERRNOS:
|
|
398
|
+
raise
|
|
399
|
+
finally:
|
|
400
|
+
os.close(fd)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _remove_staged_wrapper(staging: Path) -> None:
|
|
404
|
+
if staging.is_symlink() or not staging.is_dir():
|
|
405
|
+
return
|
|
406
|
+
for child in staging.iterdir():
|
|
407
|
+
if child.is_symlink() or child.is_file():
|
|
408
|
+
child.unlink()
|
|
409
|
+
try:
|
|
410
|
+
staging.rmdir()
|
|
411
|
+
except OSError:
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _stage_text(destination: Path, content: str) -> Path:
|
|
416
|
+
fd, temp_name = tempfile.mkstemp(
|
|
417
|
+
dir=destination.parent,
|
|
418
|
+
prefix=f".{destination.name}.",
|
|
419
|
+
suffix=".tmp",
|
|
420
|
+
)
|
|
421
|
+
temp_path = Path(temp_name)
|
|
422
|
+
try:
|
|
423
|
+
handle = os.fdopen(fd, "w", encoding="utf-8")
|
|
424
|
+
fd = -1
|
|
425
|
+
with handle:
|
|
426
|
+
handle.write(content)
|
|
427
|
+
handle.flush()
|
|
428
|
+
os.fsync(handle.fileno())
|
|
429
|
+
return temp_path
|
|
430
|
+
except Exception:
|
|
431
|
+
if fd >= 0:
|
|
432
|
+
os.close(fd)
|
|
433
|
+
temp_path.unlink(missing_ok=True)
|
|
434
|
+
raise
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _sync_auxiliaries(skill_dir: Path, target: Path) -> None:
|
|
438
|
+
expected = {child.name for child in skill_dir.iterdir() if child.name != "SKILL.md"}
|
|
439
|
+
for child in sorted(skill_dir.iterdir()):
|
|
440
|
+
if child.name == "SKILL.md":
|
|
441
|
+
continue
|
|
442
|
+
destination = target / child.name
|
|
443
|
+
if destination.is_symlink():
|
|
444
|
+
if _points_to(destination, child):
|
|
170
445
|
continue
|
|
171
|
-
|
|
172
|
-
|
|
446
|
+
continue
|
|
447
|
+
if destination.exists():
|
|
448
|
+
continue
|
|
449
|
+
destination.symlink_to(child)
|
|
450
|
+
|
|
451
|
+
source_root = skill_dir.resolve()
|
|
452
|
+
for destination in target.iterdir():
|
|
453
|
+
if destination.name in expected | {"SKILL.md", ADAPTED_MARKER}:
|
|
454
|
+
continue
|
|
455
|
+
if destination.is_symlink() and _is_relative_to(
|
|
456
|
+
_symlink_target(destination), source_root
|
|
457
|
+
):
|
|
458
|
+
destination.unlink()
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _remove_managed_wrapper(path: Path, skills_src_resolved: Path) -> bool:
|
|
462
|
+
if not _is_adapted_wrapper(path):
|
|
463
|
+
return False
|
|
464
|
+
children = list(path.iterdir())
|
|
465
|
+
for child in children:
|
|
466
|
+
if child.name in {"SKILL.md", ADAPTED_MARKER}:
|
|
467
|
+
if child.is_symlink() or not child.is_file():
|
|
468
|
+
return False
|
|
469
|
+
continue
|
|
470
|
+
if not child.is_symlink():
|
|
471
|
+
return False
|
|
472
|
+
if not _is_relative_to(_symlink_target(child), skills_src_resolved):
|
|
473
|
+
return False
|
|
474
|
+
for child in children:
|
|
475
|
+
child.unlink()
|
|
476
|
+
path.rmdir()
|
|
477
|
+
return True
|
|
173
478
|
|
|
174
479
|
|
|
175
480
|
def _is_relative_to(path: Path, parent: Path) -> bool:
|
|
@@ -194,113 +499,58 @@ def _render_frontmatter(entries: list[tuple[str, str]]) -> str:
|
|
|
194
499
|
return "\n".join(f"{key}: {value}" for key, value in entries)
|
|
195
500
|
|
|
196
501
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
502
|
+
def _adapt_body(body: str, platform: str) -> str:
|
|
503
|
+
label = _PLATFORM_LABELS[platform]
|
|
504
|
+
body = _replace_agent_calls(body, label)
|
|
505
|
+
body = body.replace("Agent Teams", f"coordinated {label}-native subagents")
|
|
506
|
+
body = body.replace("`Agent` tool", f"{label}-native subagents")
|
|
507
|
+
body = body.replace("the `Agent` tool", f"{label}-native subagents")
|
|
508
|
+
body = body.replace("Agent tool", f"{label}-native subagents")
|
|
509
|
+
for token, replacement in _semantic_replacements(platform).items():
|
|
510
|
+
body = body.replace(token, replacement)
|
|
511
|
+
if platform == "codex":
|
|
512
|
+
body = _POSITIONAL_ARGUMENT_RE.sub(
|
|
513
|
+
lambda match: f"the user-supplied positional task detail {match.group(1)}",
|
|
514
|
+
body,
|
|
515
|
+
)
|
|
516
|
+
return f"{_translation_note(platform).strip()}\n\n{body.strip()}\n"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _replace_agent_calls(body: str, label: str) -> str:
|
|
520
|
+
"""Replace balanced Agent calls without consuming surrounding markdown."""
|
|
521
|
+
rendered: list[str] = []
|
|
522
|
+
cursor = 0
|
|
523
|
+
while match := _AGENT_START_RE.search(body, cursor):
|
|
524
|
+
rendered.append(body[cursor:match.start()])
|
|
525
|
+
end = _balanced_call_end(body, match.end() - 1)
|
|
526
|
+
rendered.append(
|
|
527
|
+
f"Delegate this independent work to a suitable {label}-native subagent."
|
|
528
|
+
)
|
|
529
|
+
cursor = end
|
|
530
|
+
rendered.append(body[cursor:])
|
|
531
|
+
return "".join(rendered)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _balanced_call_end(text: str, opening_parenthesis: int) -> int:
|
|
535
|
+
depth = 1
|
|
536
|
+
quote: str | None = None
|
|
537
|
+
is_escaped = False
|
|
538
|
+
for index in range(opening_parenthesis + 1, len(text)):
|
|
539
|
+
character = text[index]
|
|
540
|
+
if quote is not None:
|
|
541
|
+
if is_escaped:
|
|
542
|
+
is_escaped = False
|
|
543
|
+
elif character == "\\":
|
|
544
|
+
is_escaped = True
|
|
545
|
+
elif character == quote:
|
|
546
|
+
quote = None
|
|
215
547
|
continue
|
|
216
|
-
if
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
body = body.replace(
|
|
226
|
-
"## MANDATORY: You MUST use the Agent tool",
|
|
227
|
-
"## MANDATORY: Use Codex subagents for delegation",
|
|
228
|
-
)
|
|
229
|
-
body = body.replace("`Agent` tool", "Codex subagent tools")
|
|
230
|
-
body = body.replace("the `Agent` tool", "Codex subagent tools")
|
|
231
|
-
body = body.replace("Agent tool", "Codex subagent tools")
|
|
232
|
-
body = body.replace("Agent Teams", "parallel Codex subagents")
|
|
233
|
-
body = body.replace(
|
|
234
|
-
"Requires: `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`",
|
|
235
|
-
"Requires: native Codex subagent support (`spawn_agent`, `send_input`, `wait_agent`, `close_agent`)",
|
|
236
|
-
)
|
|
237
|
-
body = body.replace(
|
|
238
|
-
"Check `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is set; warn if not",
|
|
239
|
-
"Confirm the user explicitly wants delegated/subagent execution before launching workers",
|
|
240
|
-
)
|
|
241
|
-
body = body.replace(
|
|
242
|
-
"export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1",
|
|
243
|
-
"# No extra environment variable is required in Codex",
|
|
244
|
-
)
|
|
245
|
-
body = body.replace("TaskCreate", "update_plan")
|
|
246
|
-
body = body.replace("TaskList", "update_plan")
|
|
247
|
-
body = body.replace("TaskUpdate", "update_plan")
|
|
248
|
-
body = body.replace("TaskGet", "wait_agent")
|
|
249
|
-
body = body.replace("TaskOutput", "wait_agent")
|
|
250
|
-
body = body.replace("TaskStop", "close_agent")
|
|
251
|
-
body = body.replace("SendMessage", "send_input")
|
|
252
|
-
body = body.replace("TeamCreate", "spawn_agent")
|
|
253
|
-
body = body.replace("TeamDelete", "close_agent")
|
|
254
|
-
|
|
255
|
-
body = _MULTILINE_AGENT_CALL_RE.sub(_replace_multiline_agent_call, body)
|
|
256
|
-
body = _SINGLE_AGENT_CALL_RE.sub(_replace_single_agent_call, body)
|
|
257
|
-
|
|
258
|
-
if "$ARGUMENTS" in body:
|
|
259
|
-
body = body.replace("$ARGUMENTS", f"$ARGUMENTS\n{_CODEX_NOTE.rstrip()}", 1)
|
|
260
|
-
|
|
261
|
-
return body
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _replace_single_agent_call(match: re.Match[str]) -> str:
|
|
265
|
-
role = match.group(1)
|
|
266
|
-
prompt = match.group(2)
|
|
267
|
-
agent_type = _codex_agent_type(role, prompt)
|
|
268
|
-
escaped_prompt = prompt.replace('"', "'")
|
|
269
|
-
return (
|
|
270
|
-
f'spawn_agent(agent_type="{agent_type}", fork_context=True, '
|
|
271
|
-
f'message="Act as {role}. {escaped_prompt}")'
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def _replace_multiline_agent_call(match: re.Match[str]) -> str:
|
|
276
|
-
role = match.group(1)
|
|
277
|
-
prompt = match.group(3).replace('"', "'")
|
|
278
|
-
return (
|
|
279
|
-
"spawn_agent(\n"
|
|
280
|
-
f' agent_type="{_codex_agent_type(role, prompt)}",\n'
|
|
281
|
-
" fork_context=True,\n"
|
|
282
|
-
f' message="Act as {role}. {prompt}"\n'
|
|
283
|
-
")"
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def _codex_agent_type(role: str, prompt: str) -> str:
|
|
288
|
-
role_l = role.lower()
|
|
289
|
-
prompt_l = prompt.lower()
|
|
290
|
-
if role_l == "explorer-agent":
|
|
291
|
-
return "explorer"
|
|
292
|
-
if any(token in prompt_l for token in ("read-only", "review", "audit", "trace", "map ", "analyze")):
|
|
293
|
-
return "default"
|
|
294
|
-
if any(token in prompt_l for token in (
|
|
295
|
-
"own files:", "implement", "write ", "apply", "build ", "update ",
|
|
296
|
-
"create ", "execute ", "fix", "document", "deploy",
|
|
297
|
-
)):
|
|
298
|
-
return "worker"
|
|
299
|
-
return "default"
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def _strip_quotes(value: str) -> str:
|
|
303
|
-
value = value.strip()
|
|
304
|
-
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
305
|
-
return value[1:-1]
|
|
306
|
-
return value
|
|
548
|
+
if character in {"'", '"'}:
|
|
549
|
+
quote = character
|
|
550
|
+
elif character == "(":
|
|
551
|
+
depth += 1
|
|
552
|
+
elif character == ")":
|
|
553
|
+
depth -= 1
|
|
554
|
+
if depth == 0:
|
|
555
|
+
return index + 1
|
|
556
|
+
raise ValueError(f"Unbalanced Agent call at character {opening_parenthesis}")
|