@laitszkin/apollo-toolkit 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +19 -0
- package/README.md +1 -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/README.md +2 -5
- package/codex-subagent-orchestration/SKILL.md +18 -18
- package/codex-subagent-orchestration/agents/openai.yaml +1 -1
- package/codex-subagent-orchestration/references/routing-rubric.md +1 -3
- package/package.json +1 -1
- package/codex-subagent-orchestration/references/openai-codex-subagents.md +0 -41
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,25 @@ All notable changes to this repository are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [v2.4.1] - 2026-03-19
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Tighten `codex-subagent-orchestration` so non-trivial tasks must use actual subagent tool calls when delegation is allowed, instead of stopping at prose-only delegation guidance.
|
|
11
|
+
- Require `codex-subagent-orchestration` to default to a parallel subagents workflow whenever two or more independent workstreams can run safely in parallel.
|
|
12
|
+
- Clarify runtime handoff and orchestration boundaries for delegated agents, including tool-rule, sandbox, write-scope, and isolated-review expectations.
|
|
13
|
+
|
|
14
|
+
## [v2.4.0] - 2026-03-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- 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`.
|
|
18
|
+
- Add extractor and index-sync helper scripts plus focused tests for the new Codex memory workflow.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Update `codex-subagent-orchestration` guidance, prompts, and routing notes to require explicit subagent spawning language for non-trivial tasks.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
- Remove the standalone OpenAI Codex subagent summary reference from `codex-subagent-orchestration` now that the skill documentation carries the needed guidance directly.
|
|
25
|
+
|
|
7
26
|
## [v2.3.0] - 2026-03-18
|
|
8
27
|
|
|
9
28
|
### 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
|
-
-
|
|
35
|
+
- Custom agent TOML format support
|
|
39
36
|
|
|
40
37
|
## License
|
|
41
38
|
|
|
@@ -8,39 +8,34 @@ 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
|
|
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
|
|
18
|
-
- Execution: Use this skill for nearly every non-trivial task
|
|
19
|
-
- Quality: Keep each custom agent narrow, opinionated, and non-overlapping; prefer read-only sandboxes for explorers and reviewers; avoid parallel write conflicts.
|
|
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. When current tool rules allow delegation, the agent must actually launch one or more subagents instead of only describing delegation in prose. Treat a parallel subagents workflow as the default whenever two or more independent workstreams can run safely in parallel, use a single helper agent only when there is exactly one bounded sidecar job, and 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; keep review contexts isolated from the implementation thread; 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
|
|
|
22
22
|
## Overview
|
|
23
23
|
|
|
24
24
|
This skill adds a repeatable orchestration layer on top of Codex subagents.
|
|
25
25
|
|
|
26
|
-
|
|
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
|
|
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,11 +52,9 @@ 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
|
-
Use this skill by default unless the task is genuinely trivial, such as:
|
|
57
|
+
Use this skill by default unless the task is genuinely trivial or current tool rules disallow delegation, such as:
|
|
65
58
|
|
|
66
59
|
- a one-shot factual answer with no decomposition value
|
|
67
60
|
- a single obvious command or one-line edit
|
|
@@ -75,16 +68,20 @@ Subagents are most valuable for:
|
|
|
75
68
|
- browser reproduction and debugging
|
|
76
69
|
- parallel edits across unrelated files or modules
|
|
77
70
|
|
|
78
|
-
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.
|
|
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, and do not force parallel writers.
|
|
72
|
+
|
|
73
|
+
This skill is not satisfied by merely writing that Codex should delegate later. When parallelizable sidecar work exists and delegation is allowed, the default compliant shape is a parallel subagents workflow.
|
|
79
74
|
|
|
80
75
|
## Workflow
|
|
81
76
|
|
|
82
77
|
### 1) Triage the task first
|
|
83
78
|
|
|
84
79
|
- Decide whether the task is trivial, serial-but-complex, or parallelizable.
|
|
80
|
+
- If the task is non-trivial and delegation is allowed, you must delegate at least one bounded subtask to a subagent.
|
|
81
|
+
- If the task has two or more independent read/review/exploration tracks, you must use a parallel subagents workflow rather than a single helper agent or a staged suggestion-only plan.
|
|
85
82
|
- Use subagents for most non-trivial tasks, but do not force them into tiny or tightly coupled work.
|
|
86
83
|
- Prefer one writer plus supporting read-only agents when ownership would otherwise overlap.
|
|
87
|
-
-
|
|
84
|
+
- If tool rules require explicit user intent before delegation, confirm that gate first; once satisfied, launch the chosen subagents and do not stay in suggestion-only mode.
|
|
88
85
|
|
|
89
86
|
### 2) Inspect the current agent catalog
|
|
90
87
|
|
|
@@ -154,6 +151,7 @@ Whenever you prompt a subagent, include:
|
|
|
154
151
|
- the expected summary or output format
|
|
155
152
|
- the file or module ownership boundary
|
|
156
153
|
- the stop condition if the agent hits uncertainty or overlap
|
|
154
|
+
- the instruction to stay within current tool-rule limits for delegation, sandbox, and write scope
|
|
157
155
|
|
|
158
156
|
### 6) Decompose ownership before spawning
|
|
159
157
|
|
|
@@ -170,9 +168,11 @@ Avoid combining exploration, review, and editing into one reusable agent when th
|
|
|
170
168
|
|
|
171
169
|
### 7) Orchestrate the run
|
|
172
170
|
|
|
173
|
-
-
|
|
171
|
+
- Use actual subagent tool calls when delegation is allowed; do not stop at writing that Codex should spawn agents later.
|
|
172
|
+
- State exactly how to split the work before each launch.
|
|
174
173
|
- Say whether to wait for all agents before continuing or to stage them in sequence.
|
|
175
174
|
- Ask for concise returned summaries, not raw logs.
|
|
175
|
+
- Treat single-subagent delegation as the exception path, not the default orchestration pattern.
|
|
176
176
|
|
|
177
177
|
Preferred patterns:
|
|
178
178
|
|
|
@@ -183,6 +183,7 @@ Preferred patterns:
|
|
|
183
183
|
Practical default:
|
|
184
184
|
|
|
185
185
|
- spawn 2-4 agents for a complex task
|
|
186
|
+
- spawn at least 2 agents when the task clearly contains parallelizable investigation or review tracks
|
|
186
187
|
- keep within the current `agents.max_threads`
|
|
187
188
|
- keep nesting shallow; many Codex setups leave `agents.max_depth` at 1 unless configured otherwise
|
|
188
189
|
|
|
@@ -210,5 +211,4 @@ If the task turns into one tightly coupled stream of work, stop delegating new e
|
|
|
210
211
|
Load only when needed:
|
|
211
212
|
|
|
212
213
|
- `references/custom-agent-template.toml`
|
|
213
|
-
- `references/openai-codex-subagents.md`
|
|
214
214
|
- `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
|
|
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
|
-
|
|
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,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.
|