Skip to content

Commit eca2705

Browse files
montfortclaude
andauthored
feat: fw-4.19.0 + cli-3.16.0 — Codex CLI (OpenAI) skill support (#203)
Adds a fourth parallel skill distribution under `dist/.codex/skills/` so the OpenAI Codex CLI can discover StrayMark skills. Codex rejects Claude-only frontmatter keys (`allowed-tools`) and only loads skills from `~/.codex/skills/` (user-level), not from the project tree like Claude and Gemini do — so the release also ships a new CLI subcommand that installs the project's `.codex/skills/` into the user's Codex home. Framework (fw-4.19.0): - New `dist/.codex/skills/` tree (11 skills) generated from `.claude/skills/` with minimal frontmatter (only `name` + `description`); body byte-identical. - `.codex/skills/` added to `dist/dist-manifest.yml`. - `codex-cli-v1.0` added to the AGENT-RULES `agent:` examples (EN/es/zh-CN). CLI (cli-3.16.0): - `straymark install-skills --agent <codex|claude|gemini> [--path .] [--dry-run] [--symlink]`: copies (or symlinks) every `straymark-*` skill from `<path>/.codex/skills/` into `$CODEX_HOME/skills/` (or `~/.codex/skills/`). `--agent claude|gemini` exits with guidance since those agents read from the project tree directly. - `straymark validate --agent codex`: verifies presence of `SKILL.md`, parseable YAML frontmatter, required `name`/`description`, and absence of Claude-only keys (`allowed-tools`, `argument-hint`, `model`) — a smell of someone copying skills from `.claude/`. - `cli/src/bin/gen_codex_skills`: generator binary; `--check` mode for CI to detect drift between `.claude/skills/` and `.codex/skills/`. Docs: - CHANGELOG entry for fw-4.19.0 / cli-3.16.0. - README + CLI-REFERENCE (EN + es + zh-CN): Codex column in skills table, `.codex/skills/` in multi-agent diagram, new sections for `install-skills` and `validate --agent`, Codex CLI in supported-platforms table. - Version footers bumped fw-4.18.0 → fw-4.19.0 across governance docs. Motivated by an adopter using Codex CLI for external Charter audits in Sentinel. Out of scope for this PR (tracked as follow-ups): umbrella skill, `agents/openai.yaml` per skill, tool-agnostic body rewrite, integration with Codex's `~/.codex/skills/.system/skill-installer`. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f1b5907 commit eca2705

53 files changed

Lines changed: 2666 additions & 91 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ and this project uses [independent versioning](README.md#versioning) for Framewo
77

88
---
99

10+
## Framework 4.19.0 / CLI 3.16.0 — Codex CLI (OpenAI) skill support
11+
12+
Adds first-class distribution of StrayMark skills for the **Codex CLI** (OpenAI), motivated by an adopter using Codex for external Charter audits in the Sentinel project. Codex's skill loader rejects the Claude-only `allowed-tools` frontmatter key and discovers skills only at the **user level** (`~/.codex/skills/`), not from the project tree like Claude and Gemini do. The release ships a fourth parallel skill variant generated from the Claude source, plus a new CLI command to install them.
13+
14+
### Added (Framework)
15+
16+
- **`dist/.codex/skills/` tree (11 skills)** — fourth parallel skill distribution alongside `.claude/skills/`, `.gemini/skills/`, and `.agent/workflows/`. Each `SKILL.md` keeps only the Codex-compatible frontmatter (`name`, `description`); the body is byte-identical to the Claude variant. Generated by `cargo run --bin gen_codex_skills` from the Claude source — one source of truth.
17+
- **`.codex/skills/` entry in `dist/dist-manifest.yml`**`straymark init` and `straymark update` now materialize the tree in the adopter's project alongside the existing skill directories.
18+
19+
### Added (CLI)
20+
21+
- **`straymark install-skills --agent <codex|claude|gemini> [--path .] [--dry-run] [--symlink]`** *(new subcommand)* — installs StrayMark skills into an AI agent's user-level skills directory. Currently only `--agent codex` performs work: copies (or symlinks with `--symlink`) every `straymark-*` skill from `<path>/.codex/skills/` into `$CODEX_HOME/skills/` (or `$HOME/.codex/skills/`). `--dry-run` previews without writing. `--agent claude|gemini` exits with an explanatory error because those agents read skills from the project tree directly. Re-installs replace any existing `straymark-*` directories at the target; non-`straymark-*` skills (e.g. Codex's `.system/` bundle) are left untouched.
22+
- **`straymark validate --agent codex`** — agent-targeted validation path. Inspects `~/.codex/skills/straymark-*` for: presence of `SKILL.md`, parseable YAML frontmatter, required `name`/`description`, and absence of Claude-only keys (`allowed-tools`, `argument-hint`, `model`) whose presence signals a misinstallation (someone copied from `.claude/skills/`).
23+
- **`cli/src/bin/gen_codex_skills`** — generator binary that transforms `dist/.claude/skills/*/SKILL.md``dist/.codex/skills/*/SKILL.md` with minimal frontmatter. Idempotent; supports `--check` for CI to detect drift between the two trees.
24+
25+
### Adopter guidance
26+
27+
Adopters who use Codex CLI should run `straymark update` (or update CLI to `cli-3.16.0` + framework to `fw-4.19.0`), then once per machine:
28+
29+
```bash
30+
straymark install-skills --agent codex
31+
straymark validate --agent codex # confirms the install
32+
```
33+
34+
Re-run `install-skills` after every `straymark update` to refresh skill content (or use `--symlink` once to track project changes automatically; Unix-only). Adopters who do **not** use Codex are unaffected — the `.codex/skills/` tree adds files but no CLI command runs against it without `--agent codex`.
35+
36+
---
37+
1038
## Framework 4.18.0 — Polish Charter as debt-detection pattern + "surface declaration without wiring" anti-pattern
1139

1240
Names a recurring anti-pattern surfaced empirically across a polish Charter session in the Sentinel adopter (`StrangeDaysTech/sentinel` CHARTER-19 → CHARTER-27, May 2026): **"Surface declaration without wiring"** — an artifact (env var documented in a runbook, metric instrument declared in a metrics package, URL referenced from an embedded HTML template, route marked public-by-contract) gets declared in one place while the implementation wiring lives in another place, with neither tooling nor review process correlating the two. Integration tests with mock adapters (`humatest`, in-memory event buses) systematically bypass the composed-app boot path where the gap would surface. The polish Charter — the closing Charter of an Etapa / SpecKit `Polish` Phase — is the load-bearing discovery vehicle because it exercises the documented operator runbook end-to-end against the real binary. New pattern doc + small charter-template addition formalize the convention so adopters treat the polish Charter as a debt-detection gate, not as cosmetic cleanup. Originated in [issue #199](https://github.com/StrangeDaysTech/straymark/issues/199). No CLI bump.

README.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ StrayMark uses independent version tags for each component:
276276

277277
| Component | Tag prefix | Example | Includes |
278278
| --- | --- | --- | --- |
279-
| Framework | `fw-` | `fw-4.18.0` | Templates (12 types), governance, directives, Charter template + schema |
280-
| CLI | `cli-` | `cli-3.15.0` | The `straymark` binary |
279+
| Framework | `fw-` | `fw-4.19.0` | Templates (12 types), governance, directives, Charter template + schema |
280+
| CLI | `cli-` | `cli-3.16.0` | The `straymark` binary |
281281

282282
Check installed versions with `straymark status` or `straymark about`.
283283

@@ -309,7 +309,7 @@ See [CLI Reference](https://github.com/StrangeDaysTech/straymark/blob/main/docs/
309309
```bash
310310
# Download the latest framework release ZIP from GitHub
311311
# Go to https://github.com/StrangeDaysTech/straymark/releases
312-
# and download the latest fw-* release (e.g., fw-4.18.0)
312+
# and download the latest fw-* release (e.g., fw-4.19.0)
313313
314314
# Extract and copy to your project
315315
unzip straymark-fw-*.zip -d your-project/
@@ -486,15 +486,17 @@ StrayMark includes skills for AI agents that enable **active documentation creat
486486
487487
### Available Skills
488488

489-
| Skill | Purpose | Claude | Gemini |
490-
| --- | --- | --- | --- |
491-
| `/straymark-status` | Check documentation compliance |||
492-
| `/straymark-new` | Create any document type (unified) |||
493-
| `/straymark-ailog` | Quick AILOG creation |||
494-
| `/straymark-aidec` | Quick AIDEC creation |||
495-
| `/straymark-adr` | Quick ADR creation |||
496-
| `/straymark-sec` | Security Assessment creation |||
497-
| `/straymark-mcard` | Model/System Card creation |||
489+
| Skill | Purpose | Claude | Gemini | Codex |
490+
| --- | --- | --- | --- | --- |
491+
| `/straymark-status` | Check documentation compliance ||||
492+
| `/straymark-new` | Create any document type (unified) ||||
493+
| `/straymark-ailog` | Quick AILOG creation ||||
494+
| `/straymark-aidec` | Quick AIDEC creation ||||
495+
| `/straymark-adr` | Quick ADR creation ||||
496+
| `/straymark-sec` | Security Assessment creation ||||
497+
| `/straymark-mcard` | Model/System Card creation ||||
498+
499+
> **Codex CLI users** *(fw-4.19.0+)*: Codex reads skills from `~/.codex/skills/` (user-level), not from the project tree. After `straymark init` (or any subsequent `straymark update`), run `straymark install-skills --agent codex` once to populate it from the project's `.codex/skills/`.
498500
499501
### Usage Examples
500502

@@ -552,7 +554,10 @@ your-project/
552554
├── .gemini/skills/ # 🔵 Gemini CLI (Google)
553555
│ ├── straymark-new/SKILL.md
554556
│ └── ...
555-
└── .claude/skills/ # 🟣 Claude Code (Anthropic)
557+
├── .claude/skills/ # 🟣 Claude Code (Anthropic)
558+
│ ├── straymark-new/SKILL.md
559+
│ └── ...
560+
└── .codex/skills/ # 🟢 Codex CLI (OpenAI) — installed to ~/.codex/skills/
556561
├── straymark-new/SKILL.md
557562
└── ...
558563
```
@@ -562,6 +567,7 @@ your-project/
562567
| `.agent/workflows/` | Antigravity, generic | VS Code/Cursor extensions | `skill-name.md` with YAML frontmatter |
563568
| `.gemini/skills/` | Gemini CLI | Google's terminal CLI | `skill-name/SKILL.md` |
564569
| `.claude/skills/` | Claude Code | Anthropic's coding agent | `skill-name/SKILL.md` |
570+
| `.codex/skills/` *(fw-4.19.0+)* | Codex CLI | OpenAI's coding agent | `skill-name/SKILL.md` (minimal frontmatter) — installed to `~/.codex/skills/` via `straymark install-skills --agent codex` |
565571

566572
> **Note**: `.agent/` is the **vendor-agnostic** standard. Agent-specific directories (`.gemini/`, `.claude/`) provide compatibility for those platforms while following their native conventions.
567573
@@ -580,6 +586,7 @@ All skill implementations are **functionally identical**—only the format diffe
580586
| Cursor | `.cursorrules` | ✅ Full support |
581587
| GitHub Copilot CLI | `.github/copilot-instructions.md` | ✅ Full support |
582588
| Gemini CLI | `GEMINI.md` | ✅ Full support |
589+
| Codex CLI (OpenAI) *(fw-4.19.0+)* | `AGENTS.md` + `~/.codex/skills/` | ✅ Full support (run `straymark install-skills --agent codex`) |
583590

584591
### Operating Systems
585592

cli/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "straymark-cli"
3-
version = "3.15.0"
3+
version = "3.16.0"
44
edition = "2021"
55
description = "CLI for StrayMark — the cognitive discipline your AI-assisted projects need"
66
license = "MIT"

cli/src/bin/gen_codex_skills.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Generator: transforms dist/.claude/skills/*/SKILL.md → dist/.codex/skills/*/SKILL.md
2+
// with a minimal frontmatter (only `name` + `description`).
3+
//
4+
// Usage:
5+
// cargo run --bin gen_codex_skills [--check] [<dist-root>]
6+
//
7+
// Default <dist-root> is `../dist` relative to CWD (i.e. run from `cli/`).
8+
// With --check, exits 1 if the destination tree differs from what would be
9+
// generated. Used in CI to keep `.codex/skills/` in sync with `.claude/skills/`.
10+
11+
use std::env;
12+
use std::fs;
13+
use std::path::{Path, PathBuf};
14+
use std::process;
15+
16+
fn main() {
17+
let mut check_only = false;
18+
let mut dist_arg: Option<String> = None;
19+
for arg in env::args().skip(1) {
20+
if arg == "--check" {
21+
check_only = true;
22+
} else if !arg.starts_with("--") && dist_arg.is_none() {
23+
dist_arg = Some(arg);
24+
} else {
25+
eprintln!("unknown argument: {arg}");
26+
process::exit(2);
27+
}
28+
}
29+
let dist = PathBuf::from(dist_arg.unwrap_or_else(|| "../dist".to_string()));
30+
if let Err(e) = run(&dist, check_only) {
31+
eprintln!("error: {e}");
32+
process::exit(1);
33+
}
34+
}
35+
36+
fn run(dist: &Path, check_only: bool) -> Result<(), String> {
37+
let src = dist.join(".claude").join("skills");
38+
let dst = dist.join(".codex").join("skills");
39+
40+
if !src.is_dir() {
41+
return Err(format!("source not found: {}", src.display()));
42+
}
43+
44+
let mut produced: Vec<(PathBuf, String)> = Vec::new();
45+
let mut names: Vec<String> = Vec::new();
46+
47+
let mut entries: Vec<_> = fs::read_dir(&src)
48+
.map_err(|e| format!("read_dir {}: {e}", src.display()))?
49+
.filter_map(Result::ok)
50+
.collect();
51+
entries.sort_by_key(|e| e.file_name());
52+
53+
for entry in entries {
54+
let path = entry.path();
55+
if !path.is_dir() {
56+
continue;
57+
}
58+
let skill_md = path.join("SKILL.md");
59+
if !skill_md.exists() {
60+
continue;
61+
}
62+
let name = entry.file_name().to_string_lossy().into_owned();
63+
let content = fs::read_to_string(&skill_md)
64+
.map_err(|e| format!("read {}: {e}", skill_md.display()))?;
65+
let transformed = transform(&content);
66+
let out = dst.join(&name).join("SKILL.md");
67+
produced.push((out, transformed));
68+
names.push(name);
69+
}
70+
71+
if check_only {
72+
let mut drift = Vec::new();
73+
for (out, expected) in &produced {
74+
let actual = fs::read_to_string(out).unwrap_or_default();
75+
if &actual != expected {
76+
drift.push(out.clone());
77+
}
78+
}
79+
// Also flag extra files in dst that aren't in produced.
80+
if dst.is_dir() {
81+
for entry in fs::read_dir(&dst).map_err(|e| e.to_string())?.flatten() {
82+
let p = entry.path();
83+
if !p.is_dir() {
84+
continue;
85+
}
86+
let n = entry.file_name().to_string_lossy().into_owned();
87+
if !names.contains(&n) {
88+
drift.push(p.join("SKILL.md"));
89+
}
90+
}
91+
}
92+
if drift.is_empty() {
93+
println!("Codex skills are in sync ({} skills).", produced.len());
94+
return Ok(());
95+
}
96+
eprintln!("Codex skills out of sync. Run: cargo run --bin gen_codex_skills");
97+
for p in drift {
98+
eprintln!(" - {}", p.display());
99+
}
100+
process::exit(1);
101+
}
102+
103+
// Clean dst and write fresh.
104+
if dst.exists() {
105+
fs::remove_dir_all(&dst)
106+
.map_err(|e| format!("remove_dir_all {}: {e}", dst.display()))?;
107+
}
108+
fs::create_dir_all(&dst).map_err(|e| format!("create_dir_all {}: {e}", dst.display()))?;
109+
for (out, content) in &produced {
110+
if let Some(parent) = out.parent() {
111+
fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
112+
}
113+
fs::write(out, content).map_err(|e| format!("write {}: {e}", out.display()))?;
114+
}
115+
println!("Generated {} Codex skills in {}", produced.len(), dst.display());
116+
for n in &names {
117+
println!(" - {n}");
118+
}
119+
Ok(())
120+
}
121+
122+
/// Strip Claude-specific frontmatter keys, keeping only `name` and `description`.
123+
fn transform(content: &str) -> String {
124+
let lines: Vec<&str> = content.lines().collect();
125+
if lines.first().map(|l| l.trim()) != Some("---") {
126+
return content.to_string();
127+
}
128+
let close_idx = match lines.iter().enumerate().skip(1).find(|(_, l)| l.trim() == "---") {
129+
Some((i, _)) => i,
130+
None => return content.to_string(),
131+
};
132+
let fm = &lines[1..close_idx];
133+
let body = &lines[close_idx + 1..];
134+
135+
let mut out = String::new();
136+
out.push_str("---\n");
137+
let mut in_kept_value = false;
138+
for line in fm {
139+
let trimmed = line.trim_start();
140+
// Top-level key line (no leading whitespace, contains colon before any space).
141+
let is_top_level_key = line.len() == trimmed.len() && line.contains(':');
142+
if is_top_level_key {
143+
let key = trimmed.split(':').next().unwrap_or("");
144+
if key == "name" || key == "description" {
145+
out.push_str(line);
146+
out.push('\n');
147+
in_kept_value = true;
148+
} else {
149+
in_kept_value = false;
150+
}
151+
} else if in_kept_value {
152+
// Continuation of a kept multiline value (indented line).
153+
out.push_str(line);
154+
out.push('\n');
155+
}
156+
// Otherwise: continuation of a dropped key — skip.
157+
}
158+
out.push_str("---\n");
159+
for line in body {
160+
out.push_str(line);
161+
out.push('\n');
162+
}
163+
out
164+
}
165+
166+
#[cfg(test)]
167+
mod tests {
168+
use super::transform;
169+
170+
#[test]
171+
fn keeps_name_and_description_only() {
172+
let input = "---\nname: foo\ndescription: bar\nallowed-tools: Read, Write\nargument-hint: \"X\"\n---\n\n# Body\n";
173+
let out = transform(input);
174+
assert!(out.contains("name: foo"));
175+
assert!(out.contains("description: bar"));
176+
assert!(!out.contains("allowed-tools"));
177+
assert!(!out.contains("argument-hint"));
178+
assert!(out.contains("# Body"));
179+
}
180+
181+
#[test]
182+
fn no_frontmatter_passthrough() {
183+
let input = "# No frontmatter\nbody\n";
184+
assert_eq!(transform(input), input);
185+
}
186+
187+
#[test]
188+
fn drops_unknown_keys() {
189+
let input = "---\nname: a\nmodel: claude-opus\ndescription: b\n---\nbody\n";
190+
let out = transform(input);
191+
assert!(!out.contains("model:"));
192+
assert!(out.contains("name: a"));
193+
assert!(out.contains("description: b"));
194+
}
195+
}

0 commit comments

Comments
 (0)