@laitszkin/apollo-toolkit 2.2.0 → 2.4.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/AGENTS.md +2 -0
- package/CHANGELOG.md +22 -0
- package/README.md +2 -0
- package/codex-memory-manager/LICENSE +21 -0
- package/codex-memory-manager/README.md +54 -0
- package/codex-memory-manager/SKILL.md +124 -0
- package/codex-memory-manager/agents/openai.yaml +4 -0
- package/codex-memory-manager/scripts/extract_recent_conversations.py +369 -0
- package/codex-memory-manager/scripts/sync_memory_index.py +130 -0
- package/codex-memory-manager/tests/test_extract_recent_conversations.py +176 -0
- package/codex-memory-manager/tests/test_sync_memory_index.py +84 -0
- package/codex-subagent-orchestration/LICENSE +21 -0
- package/codex-subagent-orchestration/README.md +39 -0
- package/codex-subagent-orchestration/SKILL.md +206 -0
- package/codex-subagent-orchestration/agents/openai.yaml +6 -0
- package/codex-subagent-orchestration/references/custom-agent-template.toml +40 -0
- package/codex-subagent-orchestration/references/routing-rubric.md +100 -0
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Synchronize a normalized memory index section into ~/.codex/AGENTS.md."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
START_MARKER = "<!-- codex-memory-manager:start -->"
|
|
12
|
+
END_MARKER = "<!-- codex-memory-manager:end -->"
|
|
13
|
+
DEFAULT_SECTION_TITLE = "## User Memory Index"
|
|
14
|
+
DEFAULT_INSTRUCTIONS = [
|
|
15
|
+
"Before starting work, review the index below and open any relevant user preference files.",
|
|
16
|
+
"When a new preference category appears, create or update the matching memory file and refresh this index.",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_args() -> argparse.Namespace:
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
description="Sync the Codex user memory index section inside AGENTS.md",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--agents-file",
|
|
26
|
+
default="~/.codex/AGENTS.md",
|
|
27
|
+
help="Path to AGENTS.md (default: ~/.codex/AGENTS.md)",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--memory-dir",
|
|
31
|
+
default="~/.codex/memory",
|
|
32
|
+
help="Directory that stores memory markdown files (default: ~/.codex/memory)",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--section-title",
|
|
36
|
+
default=DEFAULT_SECTION_TITLE,
|
|
37
|
+
help=f"Heading to use for the index section (default: {DEFAULT_SECTION_TITLE!r})",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--instruction-line",
|
|
41
|
+
action="append",
|
|
42
|
+
dest="instruction_lines",
|
|
43
|
+
help="Instruction line to place before the index bullets. Repeat to add more lines.",
|
|
44
|
+
)
|
|
45
|
+
return parser.parse_args()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def title_from_memory_file(path: Path) -> str:
|
|
49
|
+
try:
|
|
50
|
+
content = path.read_text(encoding="utf-8")
|
|
51
|
+
except OSError:
|
|
52
|
+
return path.stem.replace("-", " ").title()
|
|
53
|
+
|
|
54
|
+
for line in content.splitlines():
|
|
55
|
+
stripped = line.strip()
|
|
56
|
+
if stripped.startswith("# "):
|
|
57
|
+
return stripped[2:].strip() or path.stem.replace("-", " ").title()
|
|
58
|
+
|
|
59
|
+
return path.stem.replace("-", " ").title()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def iter_memory_files(memory_dir: Path) -> Iterable[Path]:
|
|
63
|
+
if not memory_dir.exists() or not memory_dir.is_dir():
|
|
64
|
+
return []
|
|
65
|
+
return sorted(
|
|
66
|
+
(path for path in memory_dir.glob("*.md") if path.is_file()),
|
|
67
|
+
key=lambda path: path.name.lower(),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render_section(memory_files: list[Path], section_title: str, instruction_lines: list[str]) -> str:
|
|
72
|
+
lines = [START_MARKER, section_title.strip(), ""]
|
|
73
|
+
|
|
74
|
+
cleaned_instructions = [line.strip() for line in instruction_lines if line and line.strip()]
|
|
75
|
+
for line in cleaned_instructions:
|
|
76
|
+
lines.append(line)
|
|
77
|
+
if cleaned_instructions:
|
|
78
|
+
lines.append("")
|
|
79
|
+
|
|
80
|
+
if memory_files:
|
|
81
|
+
entries = sorted(
|
|
82
|
+
((title_from_memory_file(path), path.expanduser().resolve()) for path in memory_files),
|
|
83
|
+
key=lambda item: (item[0].lower(), str(item[1]).lower()),
|
|
84
|
+
)
|
|
85
|
+
for title, path in entries:
|
|
86
|
+
lines.append(f"- [{title}]({path})")
|
|
87
|
+
else:
|
|
88
|
+
lines.append("- No memory files are currently indexed.")
|
|
89
|
+
|
|
90
|
+
lines.append(END_MARKER)
|
|
91
|
+
return "\n".join(lines)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def remove_existing_section(content: str) -> str:
|
|
95
|
+
pattern = re.compile(
|
|
96
|
+
rf"\n*{re.escape(START_MARKER)}.*?{re.escape(END_MARKER)}\n*",
|
|
97
|
+
re.DOTALL,
|
|
98
|
+
)
|
|
99
|
+
return re.sub(pattern, "\n\n", content).rstrip()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def sync_agents_file(agents_file: Path, section_text: str) -> None:
|
|
103
|
+
agents_file.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
try:
|
|
105
|
+
original = agents_file.read_text(encoding="utf-8")
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
original = ""
|
|
108
|
+
|
|
109
|
+
base = remove_existing_section(original)
|
|
110
|
+
if base:
|
|
111
|
+
updated = f"{base}\n\n{section_text}\n"
|
|
112
|
+
else:
|
|
113
|
+
updated = f"{section_text}\n"
|
|
114
|
+
agents_file.write_text(updated, encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main() -> int:
|
|
118
|
+
args = parse_args()
|
|
119
|
+
agents_file = Path(args.agents_file).expanduser()
|
|
120
|
+
memory_dir = Path(args.memory_dir).expanduser()
|
|
121
|
+
instruction_lines = args.instruction_lines or DEFAULT_INSTRUCTIONS
|
|
122
|
+
section_text = render_section(list(iter_memory_files(memory_dir)), args.section_title, instruction_lines)
|
|
123
|
+
sync_agents_file(agents_file, section_text)
|
|
124
|
+
print(f"SYNCED_AGENTS_FILE={agents_file.resolve()}")
|
|
125
|
+
print(f"MEMORY_FILES_INDEXED={len(list(iter_memory_files(memory_dir)))}")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Edge-case tests for extract_recent_conversations.py."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import unittest
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SCRIPT_PATH = (
|
|
16
|
+
Path(__file__).resolve().parents[1] / "scripts" / "extract_recent_conversations.py"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def write_session(path: Path, timestamp: datetime) -> None:
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
entries = [
|
|
23
|
+
{
|
|
24
|
+
"type": "session_meta",
|
|
25
|
+
"payload": {"timestamp": timestamp.isoformat()},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "event_msg",
|
|
29
|
+
"payload": {"type": "user_message", "message": f"user:{path.stem}"},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "event_msg",
|
|
33
|
+
"payload": {"type": "agent_message", "message": f"assistant:{path.stem}"},
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
with path.open("w", encoding="utf-8") as handle:
|
|
37
|
+
for entry in entries:
|
|
38
|
+
handle.write(json.dumps(entry) + "\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_extractor(
|
|
42
|
+
sessions_dir: Path,
|
|
43
|
+
archived_dir: Path | None = None,
|
|
44
|
+
*extra_args: str,
|
|
45
|
+
) -> str:
|
|
46
|
+
cmd = [
|
|
47
|
+
sys.executable,
|
|
48
|
+
str(SCRIPT_PATH),
|
|
49
|
+
"--sessions-dir",
|
|
50
|
+
str(sessions_dir),
|
|
51
|
+
]
|
|
52
|
+
if archived_dir is not None:
|
|
53
|
+
cmd.extend(["--archived-sessions-dir", str(archived_dir)])
|
|
54
|
+
cmd.extend(extra_args)
|
|
55
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
56
|
+
return result.stdout
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ExtractRecentConversationsTests(unittest.TestCase):
|
|
60
|
+
def test_default_lookback_covers_last_24_hours(self) -> None:
|
|
61
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
62
|
+
sessions_root = Path(tmp) / "sessions"
|
|
63
|
+
now = datetime.now(timezone.utc)
|
|
64
|
+
write_session(sessions_root / "recent.jsonl", now - timedelta(hours=3))
|
|
65
|
+
|
|
66
|
+
output = run_extractor(sessions_root)
|
|
67
|
+
|
|
68
|
+
self.assertIn("RECENT_CONVERSATIONS_FOUND=1", output)
|
|
69
|
+
self.assertIn("LOOKBACK_MINUTES=1440", output)
|
|
70
|
+
|
|
71
|
+
def test_limit_zero_is_treated_as_unlimited(self) -> None:
|
|
72
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
73
|
+
sessions_root = Path(tmp) / "sessions"
|
|
74
|
+
now = datetime.now(timezone.utc)
|
|
75
|
+
write_session(sessions_root / "a.jsonl", now - timedelta(minutes=5))
|
|
76
|
+
write_session(sessions_root / "b.jsonl", now - timedelta(minutes=10))
|
|
77
|
+
write_session(sessions_root / "c.jsonl", now - timedelta(minutes=15))
|
|
78
|
+
|
|
79
|
+
output = run_extractor(
|
|
80
|
+
sessions_root,
|
|
81
|
+
None,
|
|
82
|
+
"--lookback-minutes",
|
|
83
|
+
"60",
|
|
84
|
+
"--limit",
|
|
85
|
+
"0",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.assertIn("RECENT_CONVERSATIONS_FOUND=3", output)
|
|
89
|
+
|
|
90
|
+
def test_archived_sessions_are_read_before_cleanup(self) -> None:
|
|
91
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
92
|
+
sessions_root = Path(tmp) / "sessions"
|
|
93
|
+
archived_root = Path(tmp) / "archived_sessions"
|
|
94
|
+
now = datetime.now(timezone.utc)
|
|
95
|
+
archived_file = archived_root / "archived.jsonl"
|
|
96
|
+
write_session(archived_file, now - timedelta(minutes=30))
|
|
97
|
+
|
|
98
|
+
output = run_extractor(
|
|
99
|
+
sessions_root,
|
|
100
|
+
archived_root,
|
|
101
|
+
"--lookback-minutes",
|
|
102
|
+
"60",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.assertIn("RECENT_CONVERSATIONS_FOUND=1", output)
|
|
106
|
+
self.assertIn("ARCHIVED_SESSIONS_INCLUDED=true", output)
|
|
107
|
+
self.assertIn("user:archived", output)
|
|
108
|
+
self.assertIn("assistant:archived", output)
|
|
109
|
+
self.assertIn("CLEANUP_REMOVED_ARCHIVED_SESSIONS=1", output)
|
|
110
|
+
self.assertFalse(archived_file.exists())
|
|
111
|
+
|
|
112
|
+
def test_cleanup_removes_only_old_sessions_from_sessions_dir(self) -> None:
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
114
|
+
sessions_root = Path(tmp) / "sessions"
|
|
115
|
+
now = datetime.now(timezone.utc)
|
|
116
|
+
recent_file = sessions_root / "recent.jsonl"
|
|
117
|
+
old_file = sessions_root / "old.jsonl"
|
|
118
|
+
write_session(recent_file, now - timedelta(hours=6))
|
|
119
|
+
write_session(old_file, now - timedelta(days=8))
|
|
120
|
+
|
|
121
|
+
output = run_extractor(
|
|
122
|
+
sessions_root,
|
|
123
|
+
None,
|
|
124
|
+
"--lookback-minutes",
|
|
125
|
+
"1440",
|
|
126
|
+
"--retention-days",
|
|
127
|
+
"7",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
self.assertIn("RECENT_CONVERSATIONS_FOUND=1", output)
|
|
131
|
+
self.assertIn("CLEANUP_REMOVED_OLD_SESSIONS=1", output)
|
|
132
|
+
self.assertTrue(recent_file.exists())
|
|
133
|
+
self.assertFalse(old_file.exists())
|
|
134
|
+
|
|
135
|
+
def test_archived_cleanup_skips_active_sessions_when_paths_overlap(self) -> None:
|
|
136
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
137
|
+
sessions_root = Path(tmp) / "sessions"
|
|
138
|
+
now = datetime.now(timezone.utc)
|
|
139
|
+
recent_file = sessions_root / "recent.jsonl"
|
|
140
|
+
write_session(recent_file, now - timedelta(minutes=5))
|
|
141
|
+
|
|
142
|
+
output = run_extractor(
|
|
143
|
+
sessions_root,
|
|
144
|
+
sessions_root,
|
|
145
|
+
"--lookback-minutes",
|
|
146
|
+
"60",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self.assertIn("RECENT_CONVERSATIONS_FOUND=1", output)
|
|
150
|
+
self.assertIn("CLEANUP_REMOVED_ARCHIVED_SESSIONS=0", output)
|
|
151
|
+
self.assertTrue(recent_file.exists())
|
|
152
|
+
|
|
153
|
+
def test_limit_one_only_returns_latest_session_across_sources(self) -> None:
|
|
154
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
155
|
+
sessions_root = Path(tmp) / "sessions"
|
|
156
|
+
archived_root = Path(tmp) / "archived_sessions"
|
|
157
|
+
now = datetime.now(timezone.utc)
|
|
158
|
+
write_session(sessions_root / "older.jsonl", now - timedelta(minutes=20))
|
|
159
|
+
newest = archived_root / "newest.jsonl"
|
|
160
|
+
write_session(newest, now - timedelta(minutes=5))
|
|
161
|
+
|
|
162
|
+
output = run_extractor(
|
|
163
|
+
sessions_root,
|
|
164
|
+
archived_root,
|
|
165
|
+
"--lookback-minutes",
|
|
166
|
+
"60",
|
|
167
|
+
"--limit",
|
|
168
|
+
"1",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self.assertIn("RECENT_CONVERSATIONS_FOUND=1", output)
|
|
172
|
+
self.assertIn("newest.jsonl", output)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
unittest.main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for sync_memory_index.py."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "sync_memory_index.py"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_sync(agents_file: Path, memory_dir: Path, *extra_args: str) -> str:
|
|
16
|
+
cmd = [
|
|
17
|
+
sys.executable,
|
|
18
|
+
str(SCRIPT_PATH),
|
|
19
|
+
"--agents-file",
|
|
20
|
+
str(agents_file),
|
|
21
|
+
"--memory-dir",
|
|
22
|
+
str(memory_dir),
|
|
23
|
+
]
|
|
24
|
+
cmd.extend(extra_args)
|
|
25
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
26
|
+
return result.stdout
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SyncMemoryIndexTests(unittest.TestCase):
|
|
30
|
+
def test_appends_memory_index_with_sorted_links(self) -> None:
|
|
31
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
32
|
+
root = Path(tmp)
|
|
33
|
+
agents_file = root / "AGENTS.md"
|
|
34
|
+
agents_file.write_text("# Base Instructions\n", encoding="utf-8")
|
|
35
|
+
memory_dir = root / "memory"
|
|
36
|
+
memory_dir.mkdir()
|
|
37
|
+
(memory_dir / "workflow-preferences.md").write_text("# Workflow Preferences\n", encoding="utf-8")
|
|
38
|
+
(memory_dir / "architecture-preferences.md").write_text("# Architecture Preferences\n", encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
run_sync(agents_file, memory_dir)
|
|
41
|
+
|
|
42
|
+
content = agents_file.read_text(encoding="utf-8")
|
|
43
|
+
self.assertIn("## User Memory Index", content)
|
|
44
|
+
self.assertIn("Before starting work, review the index below", content)
|
|
45
|
+
self.assertLess(
|
|
46
|
+
content.index("[Architecture Preferences]"),
|
|
47
|
+
content.index("[Workflow Preferences]"),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def test_replaces_existing_managed_section_without_duplication(self) -> None:
|
|
51
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
52
|
+
root = Path(tmp)
|
|
53
|
+
agents_file = root / "AGENTS.md"
|
|
54
|
+
agents_file.write_text(
|
|
55
|
+
"# Base\n\n<!-- codex-memory-manager:start -->\nold\n<!-- codex-memory-manager:end -->\n",
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
)
|
|
58
|
+
memory_dir = root / "memory"
|
|
59
|
+
memory_dir.mkdir()
|
|
60
|
+
(memory_dir / "style-preferences.md").write_text("# Style Preferences\n", encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
run_sync(agents_file, memory_dir, "--instruction-line", "Read this first.")
|
|
63
|
+
|
|
64
|
+
content = agents_file.read_text(encoding="utf-8")
|
|
65
|
+
self.assertEqual(content.count("<!-- codex-memory-manager:start -->"), 1)
|
|
66
|
+
self.assertIn("Read this first.", content)
|
|
67
|
+
self.assertNotIn("\nold\n", content)
|
|
68
|
+
|
|
69
|
+
def test_uses_filename_when_heading_is_missing(self) -> None:
|
|
70
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
71
|
+
root = Path(tmp)
|
|
72
|
+
agents_file = root / "AGENTS.md"
|
|
73
|
+
memory_dir = root / "memory"
|
|
74
|
+
memory_dir.mkdir()
|
|
75
|
+
(memory_dir / "java-preferences.md").write_text("No heading here\n", encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
run_sync(agents_file, memory_dir)
|
|
78
|
+
|
|
79
|
+
content = agents_file.read_text(encoding="utf-8")
|
|
80
|
+
self.assertIn("[Java Preferences]", content)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
unittest.main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yamiyorunoshura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# codex-subagent-orchestration
|
|
2
|
+
|
|
3
|
+
Use Codex subagents on nearly every non-trivial task.
|
|
4
|
+
|
|
5
|
+
This skill inspects existing custom agents under `~/.codex/agents`, reuses them when they fit, creates new focused custom agents in the official Codex TOML format when they do not, and coordinates parallel subagent work for exploration, review, verification, and unrelated module edits.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- Defaults to using subagents for most non-trivial work
|
|
10
|
+
- Explicitly instructs Codex to spawn subagents for non-trivial work
|
|
11
|
+
- Reuses existing custom agents before creating new ones
|
|
12
|
+
- Persists new reusable agents to `~/.codex/agents`
|
|
13
|
+
- Enforces narrow responsibilities and a fixed `developer_instructions` template
|
|
14
|
+
- Restricts reusable subagent models to `gpt-5.4` and `gpt-5.3-codex`
|
|
15
|
+
- Keeps tightly coupled serial work in the main agent
|
|
16
|
+
|
|
17
|
+
## Project Structure
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
.
|
|
21
|
+
├── SKILL.md
|
|
22
|
+
├── LICENSE
|
|
23
|
+
├── README.md
|
|
24
|
+
├── agents/
|
|
25
|
+
│ └── openai.yaml
|
|
26
|
+
└── references/
|
|
27
|
+
├── custom-agent-template.toml
|
|
28
|
+
└── routing-rubric.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Codex app or CLI with subagent support
|
|
34
|
+
- Write access to `~/.codex/agents`
|
|
35
|
+
- Custom agent TOML format support
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
MIT. See `LICENSE` for details.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codex-subagent-orchestration
|
|
3
|
+
description: Use for almost every non-trivial Codex task. Inspect existing custom agents under `~/.codex/agents`, reuse them when they already fit, create a new focused custom agent in the official Codex TOML format when needed, and coordinate parallel subagents for exploration, review, verification, or unrelated module work while keeping tightly coupled serial work in the main agent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Codex Subagent Orchestration
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
- Required: none.
|
|
11
|
+
- Conditional: task-specific skills only when the delegated agent's job clearly benefits from them.
|
|
12
|
+
- Optional: none.
|
|
13
|
+
- Fallback: If subagent delegation is unavailable, continue in a single thread and report that orchestration was skipped. If `~/.codex/agents` does not exist, create it before persisting personal custom agents.
|
|
14
|
+
|
|
15
|
+
## Standards
|
|
16
|
+
|
|
17
|
+
- Evidence: Inspect the current task shape and the existing custom-agent catalog before creating or updating any agent.
|
|
18
|
+
- Execution: Use this skill for nearly every non-trivial task; explicitly tell Codex to spawn one or more subagents unless the task is trivial or delegation is disallowed; delegate read-heavy exploration, review, verification, and unrelated module edits; keep shared planning, conflict resolution, and final synthesis in the main agent.
|
|
19
|
+
- Quality: Keep each custom agent narrow, opinionated, and non-overlapping; prefer read-only sandboxes for explorers and reviewers; avoid parallel write conflicts.
|
|
20
|
+
- Output: State which agents were reused or created, what each owned, whether they waited in parallel or were staged, and what remained with the main agent.
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
This skill adds a repeatable orchestration layer on top of Codex subagents.
|
|
25
|
+
|
|
26
|
+
Use this custom-agent format:
|
|
27
|
+
|
|
28
|
+
- personal custom agents live in `~/.codex/agents/`
|
|
29
|
+
- project-scoped custom agents live in `.codex/agents/`
|
|
30
|
+
- each custom agent is one standalone TOML file
|
|
31
|
+
- required fields are `name`, `description`, and `developer_instructions`
|
|
32
|
+
|
|
33
|
+
This skill adds these house rules:
|
|
34
|
+
|
|
35
|
+
- use short English noun phrases normalized to snake_case for `name`
|
|
36
|
+
- use the fixed `developer_instructions` structure from `references/custom-agent-template.toml`
|
|
37
|
+
- restrict reusable subagent model choices in this repository to `gpt-5.4` and `gpt-5.3-codex`
|
|
38
|
+
|
|
39
|
+
## Model policy
|
|
40
|
+
|
|
41
|
+
Use only these reusable subagent models:
|
|
42
|
+
|
|
43
|
+
- `gpt-5.3-codex` for most coding-focused explorers, reviewers, verifiers, and bounded implementers
|
|
44
|
+
- `gpt-5.4` for the hardest tasks that need broader judgment, ambiguity handling, or heavier cross-cutting synthesis
|
|
45
|
+
|
|
46
|
+
Set `model_reasoning_effort` by task complexity:
|
|
47
|
+
|
|
48
|
+
- `low`: straightforward bounded lookups, narrow evidence gathering, or simple mechanical checks
|
|
49
|
+
- `medium`: ordinary non-trivial exploration, review, test triage, and isolated edits
|
|
50
|
+
- `high`: ambiguous debugging, multi-step review, or higher-risk implementation work
|
|
51
|
+
- `xhigh`: only for the hardest investigations or synthesis-heavy subagents where latency is justified
|
|
52
|
+
|
|
53
|
+
Prefer `gpt-5.3-codex` first for agentic coding roles. Escalate to `gpt-5.4` when the delegated job is less mechanical and more judgment-heavy.
|
|
54
|
+
|
|
55
|
+
## When To Use
|
|
56
|
+
|
|
57
|
+
Use this skill by default unless the task is genuinely trivial, such as:
|
|
58
|
+
|
|
59
|
+
- a one-shot factual answer with no decomposition value
|
|
60
|
+
- a single obvious command or one-line edit
|
|
61
|
+
- a tiny serial fix where spawning another agent would add more coordination than value
|
|
62
|
+
|
|
63
|
+
Subagents are most valuable for:
|
|
64
|
+
|
|
65
|
+
- codebase exploration and architecture mapping
|
|
66
|
+
- evidence gathering and independent review
|
|
67
|
+
- live-doc or API verification
|
|
68
|
+
- browser reproduction and debugging
|
|
69
|
+
- parallel edits across unrelated files or modules
|
|
70
|
+
|
|
71
|
+
Keep the main agent in charge when the work is highly continuous, tightly coupled, or depends on a single evolving mental model. In those cases, let subagents provide bounded context, not final ownership.
|
|
72
|
+
|
|
73
|
+
## Workflow
|
|
74
|
+
|
|
75
|
+
### 1) Triage the task first
|
|
76
|
+
|
|
77
|
+
- Decide whether the task is trivial, serial-but-complex, or parallelizable.
|
|
78
|
+
- Use subagents for most non-trivial tasks, but do not force them into tiny or tightly coupled work.
|
|
79
|
+
- Prefer one writer plus supporting read-only agents when ownership would otherwise overlap.
|
|
80
|
+
- For any non-trivial task, explicitly instruct Codex to spawn the chosen subagents unless delegation is blocked.
|
|
81
|
+
|
|
82
|
+
### 2) Inspect the current agent catalog
|
|
83
|
+
|
|
84
|
+
- Read `~/.codex/agents/*.toml` first.
|
|
85
|
+
- Read `.codex/agents/*.toml` next when the current repository has project-scoped agents.
|
|
86
|
+
- Build a quick catalog of each agent's:
|
|
87
|
+
- `name`
|
|
88
|
+
- `description`
|
|
89
|
+
- tool or MCP surface
|
|
90
|
+
- sandbox mode
|
|
91
|
+
- effective responsibility
|
|
92
|
+
- Reuse an existing agent when its responsibility already fits the task without stretching into adjacent work.
|
|
93
|
+
|
|
94
|
+
### 3) Decide reuse vs create
|
|
95
|
+
|
|
96
|
+
Reuse an existing custom agent when all of the following are true:
|
|
97
|
+
|
|
98
|
+
- its `description` matches the delegated job
|
|
99
|
+
- its `developer_instructions` already enforce the right boundaries
|
|
100
|
+
- its tools, sandbox, and model profile are suitable
|
|
101
|
+
- using it will not create role overlap with another active agent
|
|
102
|
+
|
|
103
|
+
Create a new custom agent only when:
|
|
104
|
+
|
|
105
|
+
- no existing agent owns the job cleanly
|
|
106
|
+
- the job is likely to recur on similar tasks
|
|
107
|
+
- the responsibility can be described independently from the current one-off prompt
|
|
108
|
+
|
|
109
|
+
Do not create near-duplicates. Tighten or extend an existing agent when the gap is small and the responsibility remains coherent.
|
|
110
|
+
|
|
111
|
+
### 4) Create the custom agent in the official format when needed
|
|
112
|
+
|
|
113
|
+
- Persist reusable personal agents to `~/.codex/agents/<name>.toml`.
|
|
114
|
+
- Use the file template in `references/custom-agent-template.toml`.
|
|
115
|
+
- Match the filename to the `name` field unless there is a strong reason not to.
|
|
116
|
+
- Keep `description` human-facing and routing-oriented: it should explain when Codex should use the agent.
|
|
117
|
+
- Keep `developer_instructions` stable and role-specific; do not leak current task noise into reusable instructions.
|
|
118
|
+
- Set `model` to either `gpt-5.3-codex` or `gpt-5.4`.
|
|
119
|
+
- Set `model_reasoning_effort` from actual task complexity, not from agent prestige or habit.
|
|
120
|
+
|
|
121
|
+
Naming rule for this skill:
|
|
122
|
+
|
|
123
|
+
- choose a short English noun phrase
|
|
124
|
+
- normalize it to snake_case
|
|
125
|
+
- examples: `code_mapper`, `docs_researcher`, `browser_debugger`, `payments_reviewer`
|
|
126
|
+
|
|
127
|
+
### 5) Use the fixed instruction format
|
|
128
|
+
|
|
129
|
+
Every reusable custom agent created by this skill must keep the same section order inside `developer_instructions`:
|
|
130
|
+
|
|
131
|
+
1. `# Role`
|
|
132
|
+
2. `## Use when`
|
|
133
|
+
3. `## Do not use when`
|
|
134
|
+
4. `## Inputs`
|
|
135
|
+
5. `## Workflow`
|
|
136
|
+
6. `## Output`
|
|
137
|
+
7. `## Boundaries`
|
|
138
|
+
|
|
139
|
+
The `Use when` and `Do not use when` lists are the applicability contract. Keep them concrete.
|
|
140
|
+
|
|
141
|
+
### 5.5) Use a fixed runtime handoff format
|
|
142
|
+
|
|
143
|
+
Whenever you prompt a subagent, include:
|
|
144
|
+
|
|
145
|
+
- the exact job split
|
|
146
|
+
- whether Codex should wait for all agents before continuing
|
|
147
|
+
- the expected summary or output format
|
|
148
|
+
- the file or module ownership boundary
|
|
149
|
+
- the stop condition if the agent hits uncertainty or overlap
|
|
150
|
+
|
|
151
|
+
### 6) Decompose ownership before spawning
|
|
152
|
+
|
|
153
|
+
Give each subagent one exclusive job. Good ownership boundaries include:
|
|
154
|
+
|
|
155
|
+
- `code_mapper`: map files, entry points, and dependencies
|
|
156
|
+
- `docs_researcher`: verify external docs or APIs
|
|
157
|
+
- `security_reviewer`: look for concrete exploit or hardening risks
|
|
158
|
+
- `test_reviewer`: find missing coverage and brittle assumptions
|
|
159
|
+
- `browser_debugger`: reproduce UI behavior and capture evidence
|
|
160
|
+
- `ui_fixer` or `api_fixer`: implement a bounded change after the problem is understood
|
|
161
|
+
|
|
162
|
+
Avoid combining exploration, review, and editing into one reusable agent when those responsibilities can stay separate.
|
|
163
|
+
|
|
164
|
+
### 7) Orchestrate the run
|
|
165
|
+
|
|
166
|
+
- Explicitly tell Codex to spawn the selected subagents and state exactly how to split the work.
|
|
167
|
+
- Say whether to wait for all agents before continuing or to stage them in sequence.
|
|
168
|
+
- Ask for concise returned summaries, not raw logs.
|
|
169
|
+
|
|
170
|
+
Preferred patterns:
|
|
171
|
+
|
|
172
|
+
- Parallel read-only agents for exploration, review, tests, logs, or docs.
|
|
173
|
+
- Explorer first, implementer second, reviewer third when the work is serial but benefits from bounded context.
|
|
174
|
+
- Multiple write-capable agents only when their modules and edited files do not overlap.
|
|
175
|
+
|
|
176
|
+
Practical default:
|
|
177
|
+
|
|
178
|
+
- spawn 2-4 agents for a complex task
|
|
179
|
+
- keep within the current `agents.max_threads`
|
|
180
|
+
- keep nesting shallow; many Codex setups leave `agents.max_depth` at 1 unless configured otherwise
|
|
181
|
+
|
|
182
|
+
### 8) Keep the main agent responsible for continuity
|
|
183
|
+
|
|
184
|
+
The main agent must:
|
|
185
|
+
|
|
186
|
+
- own the todo list and the overall plan
|
|
187
|
+
- decide task boundaries
|
|
188
|
+
- merge results from parallel threads
|
|
189
|
+
- resolve conflicting findings or overlapping edits
|
|
190
|
+
- perform final validation and final user-facing synthesis
|
|
191
|
+
|
|
192
|
+
If the task turns into one tightly coupled stream of work, stop delegating new edits and bring execution back to the main agent.
|
|
193
|
+
|
|
194
|
+
### 9) Maintain the agent catalog after the task
|
|
195
|
+
|
|
196
|
+
- Persist any new reusable custom agent to `~/.codex/agents/`.
|
|
197
|
+
- If a newly created agent proved too broad, narrow its description and instructions before finishing.
|
|
198
|
+
- If two agents overlap heavily, keep one and tighten the other instead of letting both drift.
|
|
199
|
+
- Do not persist throwaway agents that are really just one-off prompts.
|
|
200
|
+
|
|
201
|
+
## References
|
|
202
|
+
|
|
203
|
+
Load only when needed:
|
|
204
|
+
|
|
205
|
+
- `references/custom-agent-template.toml`
|
|
206
|
+
- `references/routing-rubric.md`
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Codex Subagent Orchestration"
|
|
3
|
+
short_description: "Reuse or create focused Codex custom agents for most non-trivial tasks"
|
|
4
|
+
default_prompt: "Use $codex-subagent-orchestration for almost every non-trivial task: explicitly instruct Codex to spawn the needed subagents, inspect existing custom agents under `~/.codex/agents` and `.codex/agents`, reuse a focused agent when one already fits, otherwise create a new reusable custom agent in TOML format with a narrow role, noun-phrase snake_case name, explicit task applicability lists, and fixed developer-instructions sections, then coordinate those spawned subagents for exploration, review, verification, or unrelated module edits while keeping tightly coupled serial work and final synthesis in the main agent. Persist any new reusable agents to `~/.codex/agents`."
|
|
5
|
+
policy:
|
|
6
|
+
allow_implicit_invocation: true
|