Skip to content

Commit 8486e58

Browse files
committed
Add CI checks, FAQ, and banner fix
1 parent 240d952 commit 8486e58

4 files changed

Lines changed: 210 additions & 5 deletions

File tree

.github/workflows/validate.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Validate Skill
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
validate:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Check out repository
12+
uses: actions/checkout@v4
13+
14+
- name: Set up Python
15+
uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.11"
18+
19+
- name: Validate skill metadata and scripts
20+
run: python3 scripts/validate_skill.py
21+
22+
- name: Smoke-test installer
23+
run: python3 scripts/install_skill.py --dest /tmp/codex-thread-rescue-install

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,36 @@ python3 scripts/repair_codex_desktop_threads.py --cwd /absolute/path/to/project
8989
- The visibility check relies on `codex app-server`.
9090
- On non-macOS setups, skip `--restart-desktop` and relaunch Desktop manually if needed.
9191

92+
## Repository checks
93+
94+
Run the same basic validation locally that GitHub Actions runs on every push and pull request:
95+
96+
```bash
97+
python3 scripts/validate_skill.py
98+
```
99+
100+
## FAQ
101+
102+
### I installed the repo, but the skill still does not show up in Codex Desktop
103+
104+
Make sure the installed folder is exactly `~/.codex/skills/codex--thread-rescue--skill`, then restart Codex Desktop. If you cloned the repo somewhere else first, run `python3 scripts/install_skill.py` from the repo root to copy the skill into the right place.
105+
106+
### Why do old threads exist on disk but not in the left sidebar?
107+
108+
This usually means Desktop visibility is filtering them out even though the thread rows still exist in `state_5.sqlite`. The most common causes are stale global state pins, missing workspace hints, or provider-filtered visibility.
109+
110+
### Is it safe to run?
111+
112+
Yes, the default mode is read-only. Use `--print-json` or a plain dry run first, then only add `--apply` if the summary matches what you expect. Before any write, the repair script creates timestamped backups.
113+
114+
### Do I need to use the script manually every time?
115+
116+
No. Once the repo is installed as a skill, you can ask Codex directly with a prompt like `Use $codex--thread-rescue--skill to restore missing local Codex Desktop threads for /absolute/path/to/project.`
117+
118+
### Does this work outside macOS?
119+
120+
Mostly yes for diagnosis and repairs, but the Desktop restart helper is macOS-oriented. On other systems, skip `--restart-desktop` and relaunch the app yourself if needed.
121+
92122
## Notes
93123

94124
- The script is read-only unless `--apply` is provided.

assets/codex-thread-rescue-banner.svg

Lines changed: 8 additions & 5 deletions
Loading

scripts/validate_skill.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import re
7+
import subprocess
8+
import sys
9+
from pathlib import Path
10+
11+
12+
FRONTMATTER_PATTERN = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
13+
FIELD_PATTERN = re.compile(r"^([A-Za-z0-9_]+):\s*(.+?)\s*$")
14+
15+
16+
def parse_args() -> argparse.Namespace:
17+
parser = argparse.ArgumentParser(
18+
description="Validate the packaged Codex skill metadata and smoke-test bundled scripts.",
19+
)
20+
parser.add_argument(
21+
"--repo-root",
22+
default=str(Path(__file__).resolve().parents[1]),
23+
help="Skill repository root. Defaults to the parent of this script.",
24+
)
25+
return parser.parse_args()
26+
27+
28+
def fail(message: str) -> None:
29+
print(f"[FAIL] {message}")
30+
raise SystemExit(1)
31+
32+
33+
def parse_skill_frontmatter(skill_path: Path) -> dict[str, str]:
34+
content = skill_path.read_text()
35+
match = FRONTMATTER_PATTERN.match(content)
36+
if not match:
37+
fail(f"{skill_path} is missing a valid YAML-style frontmatter block")
38+
39+
result: dict[str, str] = {}
40+
for line in match.group(1).splitlines():
41+
if ":" not in line:
42+
continue
43+
key, value = line.split(":", 1)
44+
result[key.strip()] = value.strip()
45+
return result
46+
47+
48+
def parse_openai_yaml(yaml_path: Path) -> dict[str, dict[str, str]]:
49+
sections: dict[str, dict[str, str]] = {}
50+
current_section: str | None = None
51+
52+
for raw_line in yaml_path.read_text().splitlines():
53+
if not raw_line.strip():
54+
continue
55+
if not raw_line.startswith(" "):
56+
if not raw_line.endswith(":"):
57+
fail(f"{yaml_path} has an invalid top-level line: {raw_line}")
58+
current_section = raw_line[:-1]
59+
sections[current_section] = {}
60+
continue
61+
62+
if current_section is None:
63+
fail(f"{yaml_path} has indented fields before a top-level section")
64+
65+
match = FIELD_PATTERN.match(raw_line.strip())
66+
if not match:
67+
fail(f"{yaml_path} has an invalid field line: {raw_line}")
68+
key, value = match.groups()
69+
if value.startswith('"') and value.endswith('"'):
70+
value = value[1:-1]
71+
sections[current_section][key] = value
72+
73+
return sections
74+
75+
76+
def require_file(path: Path) -> None:
77+
if not path.is_file():
78+
fail(f"Required file is missing: {path}")
79+
80+
81+
def run_help(command: list[str], cwd: Path) -> None:
82+
result = subprocess.run(command, cwd=str(cwd), capture_output=True, text=True)
83+
if result.returncode != 0:
84+
details = result.stderr.strip() or result.stdout.strip()
85+
fail(f"Command failed: {' '.join(command)}\n{details}")
86+
87+
88+
def main() -> int:
89+
args = parse_args()
90+
repo_root = Path(args.repo_root).resolve()
91+
92+
skill_path = repo_root / "SKILL.md"
93+
readme_path = repo_root / "README.md"
94+
openai_path = repo_root / "agents" / "openai.yaml"
95+
install_script = repo_root / "scripts" / "install_skill.py"
96+
repair_script = repo_root / "scripts" / "repair_codex_desktop_threads.py"
97+
banner_path = repo_root / "assets" / "codex-thread-rescue-banner.svg"
98+
icon_path = repo_root / "assets" / "codex-thread-rescue-icon.svg"
99+
logo_path = repo_root / "assets" / "codex-thread-rescue-logo.svg"
100+
101+
for path in [skill_path, readme_path, openai_path, install_script, repair_script, banner_path, icon_path, logo_path]:
102+
require_file(path)
103+
104+
frontmatter = parse_skill_frontmatter(skill_path)
105+
if frontmatter.get("name") != "codex--thread-rescue--skill":
106+
fail("SKILL.md frontmatter name must be codex--thread-rescue--skill")
107+
if "description" not in frontmatter or len(frontmatter["description"]) < 40:
108+
fail("SKILL.md frontmatter description is missing or too short")
109+
110+
readme_text = readme_path.read_text()
111+
for needle in [
112+
"$codex--thread-rescue--skill",
113+
"scripts/install_skill.py",
114+
"## FAQ",
115+
"## Quick start",
116+
]:
117+
if needle not in readme_text:
118+
fail(f"README.md is missing expected content: {needle}")
119+
120+
sections = parse_openai_yaml(openai_path)
121+
interface = sections.get("interface", {})
122+
policy = sections.get("policy", {})
123+
124+
required_interface = {
125+
"display_name": "Codex Thread Rescue",
126+
"short_description": "Bring back missing Codex Desktop threads",
127+
"icon_small": "./assets/codex-thread-rescue-icon.svg",
128+
"icon_large": "./assets/codex-thread-rescue-logo.svg",
129+
"brand_color": "#7FF2C4",
130+
}
131+
for key, expected in required_interface.items():
132+
if interface.get(key) != expected:
133+
fail(f"agents/openai.yaml interface.{key} must be {expected!r}")
134+
135+
default_prompt = interface.get("default_prompt", "")
136+
if "$codex--thread-rescue--skill" not in default_prompt:
137+
fail("agents/openai.yaml default_prompt must mention $codex--thread-rescue--skill")
138+
if policy.get("allow_implicit_invocation") != "true":
139+
fail("agents/openai.yaml policy.allow_implicit_invocation must be true")
140+
141+
run_help(["python3", str(install_script), "--help"], repo_root)
142+
run_help(["python3", str(repair_script), "--help"], repo_root)
143+
144+
print("[OK] Skill metadata and bundled scripts validated successfully.")
145+
return 0
146+
147+
148+
if __name__ == "__main__":
149+
raise SystemExit(main())

0 commit comments

Comments
 (0)