Skip to content

Commit 329865c

Browse files
committed
feat(codex): add daemonized runtime skills and scorecards
1 parent 74a8e8b commit 329865c

15 files changed

Lines changed: 4620 additions & 202 deletions

docs/commands/ai.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ f codex open "continue the deploy work"
8383
f codex open "resume latest"
8484
f codex open --path ~/work/example-project "what was I doing here"
8585
f codex resolve "https://linear.app/fl2024008/project/llm-proxy-v1-6cd0a041bd76/overview" --json
86+
f codex doctor --path ~/work/example-project
8687
```
8788

8889
Behavior:
@@ -94,6 +95,7 @@ Behavior:
9495
- otherwise: start a new session with the raw query and no extra wrapper text
9596

9697
This keeps prompt cost flat unless Flow has a strong reason to recover or unroll context.
98+
Use `f codex doctor` to confirm whether wrapper transport, runtime skills, and context budgets are actually active for the current repo.
9799

98100
### Optional `flow.toml` resolver config
99101

@@ -102,11 +104,13 @@ You can teach `f codex open` and `f codex resolve` to unroll repo-specific refer
102104
```toml
103105
[codex]
104106
auto_resolve_references = true
107+
prompt_context_budget_chars = 900
108+
max_resolved_references = 1
105109

106110
[[codex.reference_resolver]]
107111
name = "linear"
108112
match = ["https://linear.app/*/issue/*", "https://linear.app/*/project/*"]
109-
command = "forge linear inspect {{ref}} --json"
113+
command = "my-linear-tool inspect {{ref}} --json"
110114
inject_as = "linear"
111115
```
112116

@@ -116,6 +120,100 @@ Notes:
116120
- `{{ref}}`, `{{query}}`, and `{{cwd}}` are available in resolver commands
117121
- built-in Linear URL parsing works even without a custom resolver
118122
- resolver output is compacted before prompt injection
123+
- `prompt_context_budget_chars` hard-caps injected context before your request is appended
124+
- `max_resolved_references` prevents broad unrolling from bloating one turn
125+
126+
### Optional runtime skill transport
127+
128+
Flow can also materialize tiny per-launch runtime skills for current upstream Codex without forking Codex.
129+
130+
Enable it with:
131+
132+
```toml
133+
[codex]
134+
runtime_skills = true
135+
136+
[options]
137+
codex_bin = "~/code/flow/scripts/codex-flow-wrapper"
138+
```
139+
140+
Current first-slice behavior:
141+
142+
- `f codex open "write plan"` can attach a tiny plan-writing runtime skill
143+
- the runtime skill is exposed only for the launched Codex process
144+
- Flow keeps the generated runtime state under `~/.config/flow/codex/runtime`
145+
146+
Inspect or clear runtime state:
147+
148+
```bash
149+
f codex runtime show
150+
f codex runtime clear
151+
f codex doctor
152+
```
153+
154+
Built-in plan writer:
155+
156+
```bash
157+
cat <<'EOF' | f codex runtime write-plan --title "Example Plan"
158+
# Example Plan
159+
160+
- item
161+
EOF
162+
```
163+
164+
### Skill eval and background refresh
165+
166+
Flow can learn which runtime skills are actually worth injecting from local
167+
Codex usage history without replaying Codex in the hot path.
168+
169+
Useful commands:
170+
171+
```bash
172+
f codex skill-eval show --path ~/work/example-project
173+
f codex skill-eval run --path ~/work/example-project
174+
f codex skill-eval cron --limit 400 --max-targets 12 --within-hours 168
175+
f codex skill-source list --path ~/work/example-project
176+
f codex skill-source sync --path ~/work/example-project --skill find-skills
177+
```
178+
179+
What `cron` does:
180+
181+
- scans only recent logged Flow Codex events
182+
- skips missing/moved repo paths
183+
- rebuilds scorecards for a bounded number of recent repos
184+
- never launches Codex or replays network work in the background
185+
186+
For your use case, this keeps learning cheap and safe enough to run regularly.
187+
188+
### macOS launchd schedule for skill-eval
189+
190+
If you want scorecards to stay fresh automatically on macOS:
191+
192+
```bash
193+
f codex-skill-eval-launchd-install
194+
f codex-skill-eval-launchd-status
195+
f codex-skill-eval-launchd-logs
196+
```
197+
198+
Default schedule:
199+
200+
- every 30 minutes
201+
- scan up to 400 recent events
202+
- rebuild up to 12 recent repo scorecards
203+
- ignore repos not seen in the last 168 hours
204+
205+
You can tune install-time bounds:
206+
207+
```bash
208+
f codex-skill-eval-launchd-install --minutes 20 --limit 600 --max-targets 16 --within-hours 72
209+
f codex-skill-eval-launchd-install --dry-run
210+
```
211+
212+
Remove it with:
213+
214+
```bash
215+
f codex-skill-eval-launchd-uninstall
216+
```
119217

120218
### Cursor behavior
121219

docs/flow-toml-spec.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ install = ["linear"] # optional: ensure skills are installed (local ~/.codex/sk
3838
# task_skill_allow_implicit_invocation = false
3939
[codex] # optional: Codex-first open/resolve behavior
4040
# auto_resolve_references = true
41+
# prompt_context_budget_chars = 1200
42+
# max_resolved_references = 2
4143
[[codex.reference_resolver]]
4244
# name = "linear"
4345
# match = ["https://linear.app/*/issue/*", "https://linear.app/*/project/*"]
44-
# command = "forge linear inspect {{ref}} --json"
46+
# command = "my-linear-tool inspect {{ref}} --json"
4547
# inject_as = "linear"
4648
[skills.seq] # optional: seq-backed dependency skill fetching defaults
4749
# seq_repo = "~/code/seq"
@@ -139,6 +141,9 @@ fr = "f run"
139141
- `[skills.codex]`: optional Codex tuning; task skill `agents/openai.yaml` generation, post-sync force reload, and implicit invocation policy defaults.
140142
- `[codex]`: optional Codex-first control-plane settings for `f codex open` / `f codex resolve`.
141143
- `auto_resolve_references`: when true, matched resolver output is compacted and injected into new-session prompts.
144+
- `prompt_context_budget_chars`: hard cap for injected context before the raw user request is appended.
145+
- `max_resolved_references`: maximum number of resolved references Flow may inject into one prompt.
146+
- `runtime_skills`: when true, `f codex open` may materialize Flow-managed per-launch runtime skills for wrapper transports.
142147
- `[[codex.reference_resolver]]`: repo-specific reference unrollers with wildcard `match` patterns and a shell `command` template.
143148
- command templates support `{{ref}}`, `{{query}}`, and `{{cwd}}`.
144149
- `[skills.seq]`: optional defaults for `f skills fetch ...` (local seq scraper integration).
@@ -175,13 +180,19 @@ task_skill_allow_implicit_invocation = false
175180

176181
[codex]
177182
auto_resolve_references = true
183+
prompt_context_budget_chars = 900
184+
max_resolved_references = 1
185+
runtime_skills = true
178186

179187
[[codex.reference_resolver]]
180188
name = "linear"
181189
match = ["https://linear.app/*/issue/*", "https://linear.app/*/project/*"]
182-
command = "forge linear inspect {{ref}} --json"
190+
command = "my-linear-tool inspect {{ref}} --json"
183191
inject_as = "linear"
184192

193+
[options]
194+
codex_bin = "~/code/flow/scripts/codex-flow-wrapper"
195+
185196
[commit.testing]
186197
mode = "block"
187198
runner = "bun"

flow.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,26 @@ name = "ai-taskd-launchd-logs"
382382
command = "python3 ./scripts/ai-taskd-launchd.py logs $@"
383383
description = "Show ai-taskd launch agent logs"
384384

385+
[[tasks]]
386+
name = "codex-skill-eval-launchd-install"
387+
command = "python3 ./scripts/codex-skill-eval-launchd.py install $@"
388+
description = "Install scheduled Codex skill-eval scorecard refresh (launchd)"
389+
390+
[[tasks]]
391+
name = "codex-skill-eval-launchd-uninstall"
392+
command = "python3 ./scripts/codex-skill-eval-launchd.py uninstall"
393+
description = "Remove scheduled Codex skill-eval scorecard refresh (launchd)"
394+
395+
[[tasks]]
396+
name = "codex-skill-eval-launchd-status"
397+
command = "python3 ./scripts/codex-skill-eval-launchd.py status"
398+
description = "Show scheduled Codex skill-eval launch agent status"
399+
400+
[[tasks]]
401+
name = "codex-skill-eval-launchd-logs"
402+
command = "python3 ./scripts/codex-skill-eval-launchd.py logs $@"
403+
description = "Show scheduled Codex skill-eval launch agent logs"
404+
385405
[[tasks]]
386406
name = "test-args"
387407
command = "echo \"arg1=$1 arg2=$2 all=$@\""

scripts/codex-flow-wrapper

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import os
7+
import signal
8+
import subprocess
9+
import sys
10+
from pathlib import Path
11+
12+
13+
RUNTIME_PREFIX = "flow-runtime-"
14+
15+
16+
def real_codex_bin() -> str:
17+
value = os.environ.get("FLOW_CODEX_REAL_BIN", "").strip()
18+
return value or "codex"
19+
20+
21+
def agents_skill_root() -> Path:
22+
return Path.home() / ".agents" / "skills"
23+
24+
25+
def load_runtime_state() -> dict | None:
26+
raw_path = os.environ.get("FLOW_CODEX_RUNTIME_STATE", "").strip()
27+
if not raw_path:
28+
return None
29+
path = Path(raw_path).expanduser()
30+
if not path.is_file():
31+
return None
32+
return json.loads(path.read_text(encoding="utf-8"))
33+
34+
35+
def remove_path(path: Path) -> None:
36+
try:
37+
if path.is_symlink() or path.is_file():
38+
path.unlink()
39+
elif path.is_dir():
40+
for child in path.iterdir():
41+
remove_path(child)
42+
path.rmdir()
43+
except FileNotFoundError:
44+
pass
45+
46+
47+
def materialize_runtime_skills(state: dict) -> list[Path]:
48+
token = str(state.get("token", "")).strip()
49+
skills = state.get("skills", [])
50+
if not token or not isinstance(skills, list) or not skills:
51+
return []
52+
53+
root = agents_skill_root()
54+
root.mkdir(parents=True, exist_ok=True)
55+
created: list[Path] = []
56+
for skill in skills:
57+
if not isinstance(skill, dict):
58+
continue
59+
name = str(skill.get("name", "")).strip()
60+
source = str(skill.get("path", "")).strip()
61+
if not name or not source:
62+
continue
63+
source_path = Path(source).expanduser()
64+
if not source_path.is_dir():
65+
continue
66+
target = root / name
67+
if target.exists() or target.is_symlink():
68+
remove_path(target)
69+
os.symlink(source_path, target, target_is_directory=True)
70+
created.append(target)
71+
return created
72+
73+
74+
def cleanup_runtime_symlinks(paths: list[Path]) -> None:
75+
for path in paths:
76+
remove_path(path)
77+
78+
79+
def main() -> int:
80+
state = load_runtime_state()
81+
created = materialize_runtime_skills(state) if state else []
82+
83+
env = dict(os.environ)
84+
runtime_state_path = env.get("FLOW_CODEX_RUNTIME_STATE", "").strip()
85+
if runtime_state_path:
86+
env["FLOW_CODEX_RUNTIME_STATE_PATH"] = runtime_state_path
87+
env.pop("FLOW_CODEX_RUNTIME_STATE", None)
88+
proc = None
89+
90+
def forward_signal(signum: int, _frame) -> None:
91+
nonlocal proc
92+
if proc is not None:
93+
proc.send_signal(signum)
94+
95+
for signum in (signal.SIGINT, signal.SIGTERM):
96+
signal.signal(signum, forward_signal)
97+
98+
try:
99+
proc = subprocess.Popen([real_codex_bin(), *sys.argv[1:]], env=env)
100+
return proc.wait()
101+
finally:
102+
cleanup_runtime_symlinks(created)
103+
104+
105+
if __name__ == "__main__":
106+
raise SystemExit(main())

0 commit comments

Comments
 (0)