@laitszkin/apollo-toolkit 2.3.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 CHANGED
@@ -16,6 +16,7 @@ This repository enables users to install and run a curated set of reusable agent
16
16
  - Users can investigate application logs and produce evidence-backed root-cause findings.
17
17
  - Users can answer repository-backed questions with additional web research when needed.
18
18
  - Users can commit and push local changes without performing version or release work.
19
+ - Users can manage Codex user-preference memory by reviewing the last 24 hours of chats, storing categorized memory documents under `~/.codex/memory`, and syncing a memory index into `~/.codex/AGENTS.md`.
19
20
  - Users can orchestrate Codex subagents for most non-trivial tasks by reusing or creating focused custom agents under `~/.codex/agents`, then delegating exploration, review, verification, and unrelated module work while keeping tightly coupled execution in the main agent.
20
21
  - Users can research a topic deeply and produce evidence-based deliverables.
21
22
  - Users can research the latest completed market week and produce a PDF watchlist of tradeable instruments for the coming week.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this repository are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v2.4.0] - 2026-03-19
8
+
9
+ ### Added
10
+ - Add `codex-memory-manager` for reviewing the last 24 hours of Codex chats, storing durable preference memory, and syncing a managed memory index into `~/.codex/AGENTS.md`.
11
+ - Add extractor and index-sync helper scripts plus focused tests for the new Codex memory workflow.
12
+
13
+ ### Changed
14
+ - Update `codex-subagent-orchestration` guidance, prompts, and routing notes to require explicit subagent spawning language for non-trivial tasks.
15
+
16
+ ### Removed
17
+ - Remove the standalone OpenAI Codex subagent summary reference from `codex-subagent-orchestration` now that the skill documentation carries the needed guidance directly.
18
+
7
19
  ## [v2.3.0] - 2026-03-18
8
20
 
9
21
  ### Added
package/README.md CHANGED
@@ -8,6 +8,7 @@ A curated skill catalog for Codex, OpenClaw, and Trae with a managed installer t
8
8
  - analyse-app-logs
9
9
  - answering-questions-with-research
10
10
  - commit-and-push
11
+ - codex-memory-manager
11
12
  - codex-subagent-orchestration
12
13
  - deep-research-topics
13
14
  - develop-new-features
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LaiTszKin
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,54 @@
1
+ # codex-memory-manager
2
+
3
+ Persist durable user preferences from recent Codex conversations into categorized memory files and a synchronized AGENTS index.
4
+
5
+ ## Highlights
6
+
7
+ - Reads the last 24 hours of `~/.codex/sessions` and `~/.codex/archived_sessions`
8
+ - Stores categorized preference memory under `~/.codex/memory/*.md`
9
+ - Keeps a normalized memory index at the end of `~/.codex/AGENTS.md`
10
+ - Adds new index entries automatically when new preference categories appear
11
+ - Preserves the existing language already used in `~/.codex/AGENTS.md`
12
+
13
+ ## Project Structure
14
+
15
+ ```text
16
+ .
17
+ ├── SKILL.md
18
+ ├── README.md
19
+ ├── LICENSE
20
+ ├── agents/
21
+ │ └── openai.yaml
22
+ ├── scripts/
23
+ │ ├── extract_recent_conversations.py
24
+ │ └── sync_memory_index.py
25
+ └── tests/
26
+ ├── test_extract_recent_conversations.py
27
+ └── test_sync_memory_index.py
28
+ ```
29
+
30
+ ## Requirements
31
+
32
+ - Python 3.9+
33
+ - Access to `~/.codex/sessions`
34
+ - Access to `~/.codex/archived_sessions`
35
+ - Write access to `~/.codex/AGENTS.md`
36
+ - Write access to `~/.codex/memory/`
37
+
38
+ ## Quick Start
39
+
40
+ Extract the recent conversations:
41
+
42
+ ```bash
43
+ python3 scripts/extract_recent_conversations.py --lookback-minutes 1440
44
+ ```
45
+
46
+ Refresh the AGENTS memory index after updating the memory files:
47
+
48
+ ```bash
49
+ python3 scripts/sync_memory_index.py --agents-file ~/.codex/AGENTS.md --memory-dir ~/.codex/memory
50
+ ```
51
+
52
+ ## License
53
+
54
+ MIT. See `LICENSE` for details.
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: codex-memory-manager
3
+ description: Manage persistent Codex user-preference memory from recent conversation history. Use when users ask to learn from the last 24 hours of chats, update `~/.codex/AGENTS.md`, maintain `~/.codex/memory/*.md`, or sync new preference categories discovered in `~/.codex/sessions` and `~/.codex/archived_sessions`.
4
+ ---
5
+
6
+ # Codex Memory Manager
7
+
8
+ ## Dependencies
9
+
10
+ - Required: none.
11
+ - Conditional: `learn-skill-from-conversations` when the same conversation review should also evolve the skill catalog.
12
+ - Optional: none.
13
+ - Fallback: If `~/.codex/sessions`, `~/.codex/archived_sessions`, or `~/.codex/AGENTS.md` are unavailable, report the missing path and stop instead of guessing.
14
+
15
+ ## Standards
16
+
17
+ - Evidence: Derive memory only from actual recent Codex conversations, and keep each stored preference tied to concrete chat evidence.
18
+ - Execution: Extract the last 24 hours first, classify durable user preferences into memory files, then refresh the AGENTS index section.
19
+ - Quality: Ignore one-off instructions, avoid duplicating categories, and preserve the existing language and tone already used in `~/.codex/AGENTS.md`.
20
+ - Output: Report which sessions were reviewed, which memory categories were created or updated, and whether the AGENTS index changed.
21
+
22
+ ## Goal
23
+
24
+ Keep a durable, categorized memory of user preferences so future agents can quickly review relevant guidance before starting work.
25
+
26
+ ## Required Resources
27
+
28
+ - `scripts/extract_recent_conversations.py` to read the last 24 hours of Codex sessions, including archived sessions.
29
+ - `scripts/sync_memory_index.py` to maintain a normalized memory index section at the end of `~/.codex/AGENTS.md`.
30
+
31
+ ## Workflow
32
+
33
+ ### 1) Extract the last 24 hours of Codex conversations
34
+
35
+ - Run:
36
+
37
+ ```bash
38
+ python3 ~/.codex/skills/codex-memory-manager/scripts/extract_recent_conversations.py --lookback-minutes 1440
39
+ ```
40
+
41
+ - The extractor reads both `~/.codex/sessions` and `~/.codex/archived_sessions`.
42
+ - If output is exactly `NO_RECENT_CONVERSATIONS`, stop immediately and report that no memory update is needed.
43
+ - Review every returned `[USER]` and `[ASSISTANT]` block before deciding that a preference is stable.
44
+ - The extractor also cleans up stale session files after reading, matching the existing conversation-learning workflow.
45
+
46
+ ### 2) Distill only stable user preferences
47
+
48
+ - Focus on preferences that are durable and reusable, such as:
49
+ - architecture and abstraction preferences
50
+ - code style and naming preferences
51
+ - workflow preferences
52
+ - testing expectations
53
+ - language- or ecosystem-specific preferences
54
+ - reporting and communication format preferences
55
+ - Ignore transient task details, secrets, and one-off requests that are not likely to generalize.
56
+ - Prefer explicit user instructions. Use assistant behavior as supporting context only when it clearly reflects repeated user guidance.
57
+
58
+ ### 3) Classify preferences into memory documents
59
+
60
+ - Store memory files under `~/.codex/memory/*.md`.
61
+ - Reuse an existing category file when the new preference clearly belongs there.
62
+ - Create a new category file when the recent chats introduce a distinct new class of preferences. Example: if the existing files are Rust-focused and recent chats introduce stable Java preferences, add a new Java-oriented category file and index it.
63
+ - Keep filenames in kebab-case and scoped to a real category, for example:
64
+ - `architecture-preferences.md`
65
+ - `workflow-preferences.md`
66
+ - `java-preferences.md`
67
+ - Use this normalized structure inside each memory file:
68
+
69
+ ```md
70
+ # Architecture Preferences
71
+
72
+ ## Scope
73
+ User preferences about system design, reuse, abstractions, and code organization.
74
+
75
+ ## Preferences
76
+ - Prefer extending existing modules over parallel implementations.
77
+ - Applies when: adding adjacent behavior in an existing codebase.
78
+ - Evidence: repeated direction from recent Codex conversations reviewed on 2026-03-18.
79
+ - Avoid speculative abstractions and over-engineering.
80
+ - Applies when: choosing between a focused edit and a broader refactor.
81
+ - Evidence: explicit repeated user guidance in recent sessions.
82
+
83
+ ## Maintenance
84
+ - Keep entries concrete and action-guiding.
85
+ - Merge duplicates instead of restating the same preference.
86
+ - Replace older statements when newer evidence clearly supersedes them.
87
+ ```
88
+
89
+ ### 4) Refresh the AGENTS memory index at the end of `~/.codex/AGENTS.md`
90
+
91
+ - First inspect `~/.codex/AGENTS.md` and mirror its existing language in the memory section instructions.
92
+ - After updating memory files, run `scripts/sync_memory_index.py` to rewrite the managed section at the end of the file.
93
+ - The section must do both of these things explicitly:
94
+ - instruct future agents to review the index before starting work
95
+ - instruct future agents to update the matching memory files and refresh the index when a new category appears
96
+ - Example command in English AGENTS files:
97
+
98
+ ```bash
99
+ python3 ~/.codex/skills/codex-memory-manager/scripts/sync_memory_index.py \
100
+ --agents-file ~/.codex/AGENTS.md \
101
+ --memory-dir ~/.codex/memory \
102
+ --section-title "## User Memory Index" \
103
+ --instruction-line "Before starting work, review the index below and open any relevant user preference files." \
104
+ --instruction-line "When a new preference category appears, create or update the matching memory file and refresh this index."
105
+ ```
106
+
107
+ - The script writes a managed block with markdown links to every indexed memory file.
108
+ - Keep the managed block at the tail of `~/.codex/AGENTS.md`; do not scatter memory links elsewhere in the file.
109
+
110
+ ### 5) Report the memory update
111
+
112
+ - Summarize:
113
+ - how many sessions were reviewed
114
+ - which categories were created or updated
115
+ - whether a new category was introduced
116
+ - whether the AGENTS memory index changed
117
+ - If no durable preferences were found, say so explicitly and avoid creating placeholder memory files.
118
+
119
+ ## Guardrails
120
+
121
+ - Do not store secrets, tokens, credentials, or personal data that should not persist.
122
+ - Do not invent preferences when the evidence is weak or ambiguous.
123
+ - Do not create duplicate categories when a current memory document already covers the same theme.
124
+ - Do not rewrite unrelated parts of `~/.codex/AGENTS.md`; only manage the memory index block at the end.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Codex Memory Manager"
3
+ short_description: "Persist user preferences from recent Codex chats"
4
+ default_prompt: "Use $codex-memory-manager to review the last 24 hours of Codex sessions, update ~/.codex/memory/*.md, and refresh the memory index at the end of ~/.codex/AGENTS.md."
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env python3
2
+ """Extract recent Codex conversation history from Codex session stores."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from dataclasses import dataclass
9
+ from datetime import datetime, timedelta, timezone
10
+ from pathlib import Path
11
+ from typing import Iterable, List, Optional, Sequence, Tuple
12
+
13
+ DEFAULT_LOOKBACK_MINUTES = 24 * 60
14
+ DEFAULT_RETENTION_DAYS = 7
15
+
16
+
17
+ @dataclass
18
+ class SessionRecord:
19
+ path: Path
20
+ timestamp_utc: datetime
21
+ messages: Optional[List[Tuple[str, str]]] = None
22
+
23
+
24
+ def parse_iso_timestamp(raw: Optional[str]) -> Optional[datetime]:
25
+ if not raw:
26
+ return None
27
+ value = raw.strip()
28
+ if not value:
29
+ return None
30
+ if value.endswith("Z"):
31
+ value = value[:-1] + "+00:00"
32
+ try:
33
+ parsed = datetime.fromisoformat(value)
34
+ except ValueError:
35
+ return None
36
+ if parsed.tzinfo is None:
37
+ parsed = parsed.replace(tzinfo=timezone.utc)
38
+ return parsed.astimezone(timezone.utc)
39
+
40
+
41
+ def read_session_timestamp(path: Path) -> Optional[datetime]:
42
+ try:
43
+ with path.open("r", encoding="utf-8") as handle:
44
+ first_line = handle.readline().strip()
45
+ except OSError:
46
+ return None
47
+
48
+ if not first_line:
49
+ return None
50
+
51
+ try:
52
+ first_entry = json.loads(first_line)
53
+ except json.JSONDecodeError:
54
+ return None
55
+
56
+ if first_entry.get("type") != "session_meta":
57
+ return None
58
+
59
+ payload = first_entry.get("payload", {})
60
+ if not isinstance(payload, dict):
61
+ return None
62
+
63
+ return parse_iso_timestamp(payload.get("timestamp")) or parse_iso_timestamp(first_entry.get("timestamp"))
64
+
65
+
66
+ def iter_session_paths(root: Path) -> Iterable[Path]:
67
+ if not root.exists() or not root.is_dir():
68
+ return
69
+ yield from root.rglob("*.jsonl")
70
+
71
+
72
+ def find_recent_sessions(
73
+ session_roots: Sequence[Path],
74
+ cutoff_utc: datetime,
75
+ limit: Optional[int],
76
+ ) -> List[SessionRecord]:
77
+ candidates: List[SessionRecord] = []
78
+ seen_paths = set()
79
+
80
+ for root in session_roots:
81
+ for path in iter_session_paths(root):
82
+ resolved_path = path.resolve()
83
+ if resolved_path in seen_paths:
84
+ continue
85
+ seen_paths.add(resolved_path)
86
+
87
+ timestamp_utc = read_session_timestamp(path)
88
+ if timestamp_utc is None:
89
+ continue
90
+ if timestamp_utc < cutoff_utc:
91
+ continue
92
+ candidates.append(SessionRecord(path=path, timestamp_utc=timestamp_utc))
93
+
94
+ candidates.sort(key=lambda record: record.timestamp_utc, reverse=True)
95
+ if limit is None:
96
+ return candidates
97
+ return candidates[:limit]
98
+
99
+
100
+ def sanitize_text(text: str, max_chars: int) -> str:
101
+ cleaned = text.replace("\r\n", "\n").replace("\r", "\n").strip()
102
+ if max_chars <= 0:
103
+ return cleaned
104
+ if len(cleaned) <= max_chars:
105
+ return cleaned
106
+ return cleaned[: max_chars - 1].rstrip() + "..."
107
+
108
+
109
+ def looks_like_wrapper_message(text: str) -> bool:
110
+ stripped = text.strip()
111
+ if not stripped:
112
+ return True
113
+ lower = stripped.lower()
114
+ return (
115
+ stripped.startswith("# AGENTS.md instructions for")
116
+ or stripped.startswith("<environment_context>")
117
+ or "<collaboration_mode>" in lower
118
+ or stripped.startswith("<permissions instructions>")
119
+ or stripped.startswith("<app-context>")
120
+ )
121
+
122
+
123
+ def extract_text_from_content(content: Sequence[object]) -> str:
124
+ texts: List[str] = []
125
+ for part in content:
126
+ if not isinstance(part, dict):
127
+ continue
128
+ part_type = part.get("type")
129
+ if part_type in {"input_text", "output_text", "text"}:
130
+ value = part.get("text", "")
131
+ if isinstance(value, str) and value.strip():
132
+ texts.append(value)
133
+ return "\n".join(texts).strip()
134
+
135
+
136
+ def extract_messages_from_event_entries(entries: Iterable[dict], max_chars: int) -> List[Tuple[str, str]]:
137
+ messages: List[Tuple[str, str]] = []
138
+ for entry in entries:
139
+ if entry.get("type") != "event_msg":
140
+ continue
141
+ payload = entry.get("payload", {})
142
+ if not isinstance(payload, dict):
143
+ continue
144
+
145
+ payload_type = payload.get("type")
146
+ if payload_type == "user_message":
147
+ text = payload.get("message", "")
148
+ if isinstance(text, str) and text.strip():
149
+ messages.append(("user", sanitize_text(text, max_chars)))
150
+ elif payload_type == "agent_message":
151
+ text = payload.get("message", "")
152
+ if isinstance(text, str) and text.strip():
153
+ messages.append(("assistant", sanitize_text(text, max_chars)))
154
+ return messages
155
+
156
+
157
+ def extract_messages_from_response_items(entries: Iterable[dict], max_chars: int) -> List[Tuple[str, str]]:
158
+ messages: List[Tuple[str, str]] = []
159
+ for entry in entries:
160
+ if entry.get("type") != "response_item":
161
+ continue
162
+ payload = entry.get("payload", {})
163
+ if not isinstance(payload, dict):
164
+ continue
165
+ if payload.get("type") != "message":
166
+ continue
167
+
168
+ role = payload.get("role")
169
+ if role not in {"user", "assistant"}:
170
+ continue
171
+
172
+ text = extract_text_from_content(payload.get("content", []))
173
+ if not text or looks_like_wrapper_message(text):
174
+ continue
175
+ messages.append((role, sanitize_text(text, max_chars)))
176
+ return messages
177
+
178
+
179
+ def extract_session_messages(path: Path, max_chars: int) -> List[Tuple[str, str]]:
180
+ entries: List[dict] = []
181
+ try:
182
+ with path.open("r", encoding="utf-8") as handle:
183
+ for line in handle:
184
+ line = line.strip()
185
+ if not line:
186
+ continue
187
+ try:
188
+ entries.append(json.loads(line))
189
+ except json.JSONDecodeError:
190
+ continue
191
+ except OSError:
192
+ return []
193
+
194
+ event_messages = extract_messages_from_event_entries(entries, max_chars)
195
+ if event_messages:
196
+ return event_messages
197
+ return extract_messages_from_response_items(entries, max_chars)
198
+
199
+
200
+ def delete_matching_files(root: Path, predicate) -> int:
201
+ if not root.exists() or not root.is_dir():
202
+ return 0
203
+
204
+ deleted_count = 0
205
+ for path in root.rglob("*.jsonl"):
206
+ if not predicate(path):
207
+ continue
208
+ try:
209
+ path.unlink()
210
+ except OSError:
211
+ continue
212
+ deleted_count += 1
213
+ return deleted_count
214
+
215
+
216
+ def path_is_same_or_nested(path: Path, root: Optional[Path]) -> bool:
217
+ if root is None:
218
+ return False
219
+ try:
220
+ path.resolve().relative_to(root.resolve())
221
+ return True
222
+ except ValueError:
223
+ return False
224
+
225
+
226
+ def cleanup_session_history(
227
+ sessions_dir: Path,
228
+ archived_sessions_dir: Path,
229
+ retention_cutoff_utc: datetime,
230
+ ) -> Tuple[int, int]:
231
+ sessions_root = sessions_dir.resolve() if sessions_dir.exists() else None
232
+ removed_old_sessions = delete_matching_files(
233
+ sessions_dir,
234
+ lambda path: (
235
+ (timestamp := read_session_timestamp(path)) is not None
236
+ and timestamp < retention_cutoff_utc
237
+ ),
238
+ )
239
+ removed_archived_sessions = delete_matching_files(
240
+ archived_sessions_dir,
241
+ lambda path: not path_is_same_or_nested(path, sessions_root),
242
+ )
243
+ return removed_old_sessions, removed_archived_sessions
244
+
245
+
246
+ def render_text_output(
247
+ records: Sequence[SessionRecord],
248
+ lookback_minutes: int,
249
+ max_message_chars: int,
250
+ removed_old_sessions: int,
251
+ removed_archived_sessions: int,
252
+ ) -> str:
253
+ if not records:
254
+ return "NO_RECENT_CONVERSATIONS"
255
+
256
+ lines: List[str] = [
257
+ f"RECENT_CONVERSATIONS_FOUND={len(records)}",
258
+ f"LOOKBACK_MINUTES={lookback_minutes}",
259
+ "ARCHIVED_SESSIONS_INCLUDED=true",
260
+ f"CLEANUP_REMOVED_OLD_SESSIONS={removed_old_sessions}",
261
+ f"CLEANUP_REMOVED_ARCHIVED_SESSIONS={removed_archived_sessions}",
262
+ ]
263
+
264
+ for index, record in enumerate(records, start=1):
265
+ lines.append(f"=== SESSION {index} ===")
266
+ lines.append(f"TIMESTAMP_UTC={record.timestamp_utc.isoformat()}")
267
+ lines.append(f"FILE={record.path}")
268
+
269
+ messages = record.messages
270
+ if messages is None:
271
+ messages = extract_session_messages(record.path, max_message_chars)
272
+ if not messages:
273
+ lines.append("MESSAGES=NONE")
274
+ continue
275
+
276
+ for role, message in messages:
277
+ tag = "USER" if role == "user" else "ASSISTANT"
278
+ lines.append(f"[{tag}]")
279
+ lines.append(message)
280
+ lines.append(f"[/{tag}]")
281
+
282
+ return "\n".join(lines)
283
+
284
+
285
+ def parse_args() -> argparse.Namespace:
286
+ parser = argparse.ArgumentParser(
287
+ description="Extract the latest conversation history from Codex session stores",
288
+ )
289
+ parser.add_argument(
290
+ "--sessions-dir",
291
+ default="~/.codex/sessions",
292
+ help="Path to the Codex sessions directory (default: ~/.codex/sessions)",
293
+ )
294
+ parser.add_argument(
295
+ "--archived-sessions-dir",
296
+ default="~/.codex/archived_sessions",
297
+ help="Path to archived Codex sessions (default: ~/.codex/archived_sessions)",
298
+ )
299
+ parser.add_argument(
300
+ "--lookback-minutes",
301
+ type=int,
302
+ default=DEFAULT_LOOKBACK_MINUTES,
303
+ help=f"How far back to look for sessions (default: {DEFAULT_LOOKBACK_MINUTES})",
304
+ )
305
+ parser.add_argument(
306
+ "--limit",
307
+ type=int,
308
+ default=None,
309
+ help="Maximum number of sessions to return (default: all within lookback window)",
310
+ )
311
+ parser.add_argument(
312
+ "--max-message-chars",
313
+ type=int,
314
+ default=1600,
315
+ help="Maximum characters per extracted message (default: 1600)",
316
+ )
317
+ parser.add_argument(
318
+ "--retention-days",
319
+ type=int,
320
+ default=DEFAULT_RETENTION_DAYS,
321
+ help=f"Delete sessions older than this many days after reading (default: {DEFAULT_RETENTION_DAYS})",
322
+ )
323
+ return parser.parse_args()
324
+
325
+
326
+ def main() -> int:
327
+ args = parse_args()
328
+
329
+ sessions_dir = Path(args.sessions_dir).expanduser().resolve()
330
+ archived_sessions_dir = Path(args.archived_sessions_dir).expanduser().resolve()
331
+ lookback_minutes = max(args.lookback_minutes, 1)
332
+ limit = args.limit if args.limit is not None and args.limit > 0 else None
333
+ max_message_chars = max(args.max_message_chars, 100)
334
+ retention_days = max(args.retention_days, 1)
335
+ now_utc = datetime.now(timezone.utc)
336
+
337
+ if (
338
+ (not sessions_dir.exists() or not sessions_dir.is_dir())
339
+ and (not archived_sessions_dir.exists() or not archived_sessions_dir.is_dir())
340
+ ):
341
+ print("NO_RECENT_CONVERSATIONS")
342
+ return 0
343
+
344
+ cutoff_utc = now_utc - timedelta(minutes=lookback_minutes)
345
+ recent_records = find_recent_sessions((sessions_dir, archived_sessions_dir), cutoff_utc, limit)
346
+ for record in recent_records:
347
+ record.messages = extract_session_messages(record.path, max_message_chars)
348
+
349
+ retention_cutoff_utc = now_utc - timedelta(days=retention_days)
350
+ removed_old_sessions, removed_archived_sessions = cleanup_session_history(
351
+ sessions_dir,
352
+ archived_sessions_dir,
353
+ retention_cutoff_utc,
354
+ )
355
+
356
+ print(
357
+ render_text_output(
358
+ recent_records,
359
+ lookback_minutes,
360
+ max_message_chars,
361
+ removed_old_sessions,
362
+ removed_archived_sessions,
363
+ )
364
+ )
365
+ return 0
366
+
367
+
368
+ if __name__ == "__main__":
369
+ raise SystemExit(main())
@@ -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()
@@ -4,16 +4,14 @@ Use Codex subagents on nearly every non-trivial task.
4
4
 
5
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
6
 
7
- The workflow is grounded in OpenAI's Codex subagent docs and then adds a few house conventions: noun-phrase snake_case names, a fixed `developer_instructions` template, and persistence of reusable personal agents under `~/.codex/agents`.
8
-
9
7
  ## Highlights
10
8
 
11
9
  - Defaults to using subagents for most non-trivial work
10
+ - Explicitly instructs Codex to spawn subagents for non-trivial work
12
11
  - Reuses existing custom agents before creating new ones
13
12
  - Persists new reusable agents to `~/.codex/agents`
14
13
  - Enforces narrow responsibilities and a fixed `developer_instructions` template
15
14
  - Restricts reusable subagent models to `gpt-5.4` and `gpt-5.3-codex`
16
- - Distinguishes official Codex requirements from this repository's house rules
17
15
  - Keeps tightly coupled serial work in the main agent
18
16
 
19
17
  ## Project Structure
@@ -26,7 +24,6 @@ The workflow is grounded in OpenAI's Codex subagent docs and then adds a few hou
26
24
  ├── agents/
27
25
  │ └── openai.yaml
28
26
  └── references/
29
- ├── openai-codex-subagents.md
30
27
  ├── custom-agent-template.toml
31
28
  └── routing-rubric.md
32
29
  ```
@@ -35,7 +32,7 @@ The workflow is grounded in OpenAI's Codex subagent docs and then adds a few hou
35
32
 
36
33
  - Codex app or CLI with subagent support
37
34
  - Write access to `~/.codex/agents`
38
- - Current OpenAI Codex custom-agent format support
35
+ - Custom agent TOML format support
39
36
 
40
37
  ## License
41
38
 
@@ -8,14 +8,14 @@ description: Use for almost every non-trivial Codex task. Inspect existing custo
8
8
  ## Dependencies
9
9
 
10
10
  - Required: none.
11
- - Conditional: task-specific skills only when the delegated agent's job clearly benefits from them; `openai-docs` when the delegated work needs current OpenAI/Codex documentation or when Codex subagent schema and orchestration rules must be re-verified.
11
+ - Conditional: task-specific skills only when the delegated agent's job clearly benefits from them.
12
12
  - Optional: none.
13
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
14
 
15
15
  ## Standards
16
16
 
17
- - Evidence: Inspect the current task shape and the existing custom-agent catalog before creating or updating any agent, and check the latest official Codex docs before changing schema-level conventions.
18
- - Execution: Use this skill for nearly every non-trivial task; delegate read-heavy exploration, review, verification, and unrelated module edits; keep shared planning, conflict resolution, and final synthesis in the main agent.
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
19
  - Quality: Keep each custom agent narrow, opinionated, and non-overlapping; prefer read-only sandboxes for explorers and reviewers; avoid parallel write conflicts.
20
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
21
 
@@ -23,24 +23,19 @@ description: Use for almost every non-trivial Codex task. Inspect existing custo
23
23
 
24
24
  This skill adds a repeatable orchestration layer on top of Codex subagents.
25
25
 
26
- The official OpenAI basis for this skill is summarized in `references/openai-codex-subagents.md`.
27
-
28
- Follow the current OpenAI Codex custom-agent format:
26
+ Use this custom-agent format:
29
27
 
30
28
  - personal custom agents live in `~/.codex/agents/`
31
29
  - project-scoped custom agents live in `.codex/agents/`
32
30
  - each custom agent is one standalone TOML file
33
31
  - required fields are `name`, `description`, and `developer_instructions`
34
32
 
35
- This skill adds two house rules on top of the official schema:
33
+ This skill adds these house rules:
36
34
 
37
35
  - use short English noun phrases normalized to snake_case for `name`
38
36
  - use the fixed `developer_instructions` structure from `references/custom-agent-template.toml`
39
37
  - restrict reusable subagent model choices in this repository to `gpt-5.4` and `gpt-5.3-codex`
40
38
 
41
- OpenAI documents the TOML schema and examples, but it does not explicitly require noun-phrase naming. Treat noun-phrase naming as this skill's convention for clearer routing and reuse.
42
- OpenAI also documents that Codex only spawns subagents when explicitly asked. Treat invoking this skill as the explicit orchestration step for non-trivial work, while still respecting any stricter environment rule that forbids delegation.
43
-
44
39
  ## Model policy
45
40
 
46
41
  Use only these reusable subagent models:
@@ -57,8 +52,6 @@ Set `model_reasoning_effort` by task complexity:
57
52
 
58
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.
59
54
 
60
- Codex does not spontaneously delegate just because custom agents exist. Calling this skill is the explicit instruction that makes the main agent inspect the catalog, decide whether delegation is worthwhile, and then reuse or create the right subagents.
61
-
62
55
  ## When To Use
63
56
 
64
57
  Use this skill by default unless the task is genuinely trivial, such as:
@@ -84,7 +77,7 @@ Keep the main agent in charge when the work is highly continuous, tightly couple
84
77
  - Decide whether the task is trivial, serial-but-complex, or parallelizable.
85
78
  - Use subagents for most non-trivial tasks, but do not force them into tiny or tightly coupled work.
86
79
  - Prefer one writer plus supporting read-only agents when ownership would otherwise overlap.
87
- - Remember that Codex does not spawn subagents automatically; the orchestration decision must be explicit.
80
+ - For any non-trivial task, explicitly instruct Codex to spawn the chosen subagents unless delegation is blocked.
88
81
 
89
82
  ### 2) Inspect the current agent catalog
90
83
 
@@ -170,7 +163,7 @@ Avoid combining exploration, review, and editing into one reusable agent when th
170
163
 
171
164
  ### 7) Orchestrate the run
172
165
 
173
- - Tell Codex exactly how to split the work.
166
+ - Explicitly tell Codex to spawn the selected subagents and state exactly how to split the work.
174
167
  - Say whether to wait for all agents before continuing or to stage them in sequence.
175
168
  - Ask for concise returned summaries, not raw logs.
176
169
 
@@ -210,5 +203,4 @@ If the task turns into one tightly coupled stream of work, stop delegating new e
210
203
  Load only when needed:
211
204
 
212
205
  - `references/custom-agent-template.toml`
213
- - `references/openai-codex-subagents.md`
214
206
  - `references/routing-rubric.md`
@@ -1,6 +1,6 @@
1
1
  interface:
2
2
  display_name: "Codex Subagent Orchestration"
3
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: 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 the official Codex TOML format with a narrow role, noun-phrase snake_case name, explicit task applicability lists, and fixed developer-instructions sections, then coordinate 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`."
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
5
  policy:
6
6
  allow_implicit_invocation: true
@@ -18,8 +18,6 @@ Keep the task in the main agent when it is:
18
18
  - likely to create overlapping edits across the same files
19
19
  - blocked by an environment rule that disallows live delegation
20
20
 
21
- OpenAI's current Codex docs also state that subagents are explicit: Codex only spawns them when asked to do so.
22
-
23
21
  ## 2. Reuse before creating
24
22
 
25
23
  Reuse an existing custom agent when:
@@ -80,7 +78,7 @@ Every subagent handoff should include:
80
78
  - `Expected output shape`
81
79
  - `Blocking or non-blocking status`
82
80
 
83
- This follows OpenAI's documented guidance that a good subagent prompt should explain the work split, whether Codex should wait, and what summary or output to return.
81
+ Use direct spawning language, for example: "spawn 2 subagents", "spawn a code-mapping subagent and a review subagent", or "do not spawn subagents because this task is trivial".
84
82
 
85
83
  ## 7. Pick model and reasoning by complexity
86
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laitszkin/apollo-toolkit",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Apollo Toolkit npm installer for managed skill linking across Codex, OpenClaw, and Trae.",
5
5
  "license": "MIT",
6
6
  "author": "LaiTszKin",
@@ -1,41 +0,0 @@
1
- # OpenAI Codex subagents notes
2
-
3
- Verified on 2026-03-18 from the official OpenAI Codex docs:
4
-
5
- - [Subagents](https://developers.openai.com/codex/subagents)
6
- - [Subagents concepts](https://developers.openai.com/codex/concepts/subagents)
7
-
8
- ## Official Codex facts this skill depends on
9
-
10
- - Codex only spawns subagents when explicitly asked to do so.
11
- - Custom agents can live in `~/.codex/agents/` for personal reuse or `.codex/agents/` for project-scoped reuse.
12
- - Each custom agent file must define `name`, `description`, and `developer_instructions`.
13
- - The `name` field is the source of truth; matching the filename to the name is only the simplest convention.
14
- - Optional fields such as `nickname_candidates`, `model`, `model_reasoning_effort`, `sandbox_mode`, `mcp_servers`, and `skills.config` can be set per custom agent.
15
- - Custom agents inherit the parent session's runtime behavior unless the custom agent configuration narrows it further.
16
- - Global orchestration settings live under `[agents]`, including `agents.max_threads`, `agents.max_depth`, and `agents.job_max_runtime_seconds`.
17
- - OpenAI recommends parallel subagents especially for read-heavy work such as exploration, triage, tests, and summarization, and warns to be more careful with parallel write-heavy workflows.
18
- - OpenAI's current model catalog says to start with `gpt-5.4` when you are not sure which model to choose.
19
- - The current `gpt-5.4` model page says `reasoning.effort` supports `none`, `low`, `medium`, `high`, and `xhigh`.
20
- - The current `gpt-5.3-codex` model page says it is optimized for agentic coding tasks and supports `low`, `medium`, `high`, and `xhigh` reasoning effort.
21
- - The best custom agents are narrow and opinionated, with a clear job and clear boundaries.
22
-
23
- ## House conventions added by this skill
24
-
25
- These rules are not required by OpenAI, but this skill standardizes them for better reuse:
26
-
27
- - use short English noun phrases normalized to snake_case for every custom-agent `name`
28
- - keep the filename equal to the `name` unless there is a strong reason not to
29
- - use the fixed `developer_instructions` section order from `references/custom-agent-template.toml`
30
- - restrict reusable subagent model choices in this repository to `gpt-5.4` and `gpt-5.3-codex`
31
- - choose `model_reasoning_effort` from task complexity instead of pinning one static effort everywhere
32
- - treat the main agent as the owner of planning, merge decisions, and final synthesis
33
- - persist reusable personal agents to `~/.codex/agents` so similar future tasks can reuse them
34
-
35
- ## What OpenAI does not currently mandate
36
-
37
- - noun-phrase grammar for custom-agent names
38
- - one universal `developer_instructions` section layout
39
- - a policy that every task should use subagents
40
-
41
- This skill chooses those conventions as opinionated defaults for non-trivial work.