Enhance Codex support by auto-syncing prompt files, allowing spec generation without git, and documenting clearer /specify usage.
This commit is contained in:
@@ -224,9 +224,11 @@ specify init <project_name> --ai claude --ignore-agent-tools
|
|||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Codex CLI specifics
|
> Codex CLI specifics
|
||||||
> - `specify init --ai codex` ensures a workspace-level `commands/` directory exists (seeding `specify.md`, `plan.md`, `tasks.md` if necessary) because Codex CLI loads slash commands from the repo itself.
|
> - `specify init --ai codex` seeds `commands/*.md` in your repo and automatically mirrors them into `${CODEX_HOME:-~/.codex}/prompts` so Codex picks up `/specify`, `/plan`, and `/tasks` immediately.
|
||||||
|
> - If Codex was running during installation, restart the CLI once so it reloads the refreshed slash commands.
|
||||||
> - Codex persists its working memory in `AGENTS.md`; if you do not see that file yet, run `codex /init` once inside the project to generate it.
|
> - Codex persists its working memory in `AGENTS.md`; if you do not see that file yet, run `codex /init` once inside the project to generate it.
|
||||||
> - To let Codex trigger the helper scripts under `scripts/` through slash commands, open `codex /approvals` and enable “Run shell commands”.
|
> - When Codex is configured to run helper scripts, open `codex /approvals` and enable “Run shell commands”.
|
||||||
|
> - If you point `CODEX_HOME` at the project (for per-repo isolation) the installer adds ignore rules for Codex session artifacts to `.gitignore`; adjust them as needed for your workflow.
|
||||||
|
|
||||||
### **STEP 1:** Bootstrap the project
|
### **STEP 1:** Bootstrap the project
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,21 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
# Resolve repository root. Prefer git information when available, but fall back
|
||||||
|
# to the script location so the workflow still functions in repositories that
|
||||||
|
# were initialised with --no-git.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
FALLBACK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
HAS_GIT=true
|
||||||
|
else
|
||||||
|
REPO_ROOT="$FALLBACK_ROOT"
|
||||||
|
HAS_GIT=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
SPECS_DIR="$REPO_ROOT/specs"
|
SPECS_DIR="$REPO_ROOT/specs"
|
||||||
mkdir -p "$SPECS_DIR"
|
mkdir -p "$SPECS_DIR"
|
||||||
|
|
||||||
@@ -40,7 +54,11 @@ BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/
|
|||||||
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
|
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
|
||||||
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
|
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
|
||||||
|
|
||||||
git checkout -b "$BRANCH_NAME"
|
if [ "$HAS_GIT" = true ]; then
|
||||||
|
git checkout -b "$BRANCH_NAME"
|
||||||
|
else
|
||||||
|
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||||
mkdir -p "$FEATURE_DIR"
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|||||||
@@ -950,6 +950,124 @@ def ensure_workspace_commands(project_path: Path, tracker: StepTracker | None =
|
|||||||
console.print(f"[yellow]Warning: could not ensure commands directory ({exc})[/yellow]")
|
console.print(f"[yellow]Warning: could not ensure commands directory ({exc})[/yellow]")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_codex_home() -> Path:
|
||||||
|
env_value = os.environ.get("CODEX_HOME")
|
||||||
|
if env_value:
|
||||||
|
return Path(env_value).expanduser()
|
||||||
|
return Path.home() / ".codex"
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_gitignore_entries(project_path: Path, entries: list[str]) -> None:
|
||||||
|
if not entries:
|
||||||
|
return
|
||||||
|
|
||||||
|
gitignore_path = project_path / ".gitignore"
|
||||||
|
|
||||||
|
existing_text = ""
|
||||||
|
existing: set[str] = set()
|
||||||
|
if gitignore_path.exists():
|
||||||
|
try:
|
||||||
|
existing_text = gitignore_path.read_text(encoding="utf-8")
|
||||||
|
existing = {line.strip() for line in existing_text.splitlines()}
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_entries = [entry for entry in entries if entry not in existing]
|
||||||
|
if not new_entries:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with gitignore_path.open("a", encoding="utf-8") as fh:
|
||||||
|
if existing_text and not existing_text.endswith("\n"):
|
||||||
|
fh.write("\n")
|
||||||
|
for entry in new_entries:
|
||||||
|
fh.write(f"{entry}\n")
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def sync_codex_prompts(project_path: Path, tracker: StepTracker | None = None) -> None:
|
||||||
|
if tracker:
|
||||||
|
tracker.start("codex-prompts")
|
||||||
|
|
||||||
|
commands_dir = project_path / "commands"
|
||||||
|
if not commands_dir.is_dir():
|
||||||
|
if tracker:
|
||||||
|
tracker.skip("codex-prompts", "no commands directory")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
codex_home = _resolve_codex_home()
|
||||||
|
prompts_dir = (codex_home / "prompts").expanduser()
|
||||||
|
prompts_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not os.access(prompts_dir, os.W_OK):
|
||||||
|
raise PermissionError(f"Codex prompts directory not writable: {prompts_dir}")
|
||||||
|
|
||||||
|
expected: set[str] = set()
|
||||||
|
copied = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
for source in sorted(commands_dir.glob("*.md")):
|
||||||
|
if not source.is_file():
|
||||||
|
continue
|
||||||
|
dest_name = source.name
|
||||||
|
dest_path = prompts_dir / dest_name
|
||||||
|
expected.add(dest_name)
|
||||||
|
|
||||||
|
data = source.read_bytes()
|
||||||
|
if dest_path.exists():
|
||||||
|
try:
|
||||||
|
if dest_path.read_bytes() == data:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
dest_path.write_bytes(data)
|
||||||
|
copied += 1
|
||||||
|
|
||||||
|
# Clean up any legacy spec-kit-prefixed prompts from earlier installer versions
|
||||||
|
for legacy in prompts_dir.glob("spec-kit-*.md"):
|
||||||
|
try:
|
||||||
|
legacy.unlink()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
detail_bits = []
|
||||||
|
if copied:
|
||||||
|
detail_bits.append(f"{copied} updated")
|
||||||
|
if skipped:
|
||||||
|
detail_bits.append(f"{skipped} unchanged")
|
||||||
|
detail = ", ".join(detail_bits) if detail_bits else "ok"
|
||||||
|
|
||||||
|
if tracker:
|
||||||
|
tracker.complete("codex-prompts", detail)
|
||||||
|
|
||||||
|
# If CODEX_HOME lives inside this project, make sure generated files stay untracked
|
||||||
|
try:
|
||||||
|
codex_home_relative = codex_home.resolve().relative_to(project_path.resolve())
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
codex_prefix = codex_home_relative.as_posix()
|
||||||
|
if codex_prefix == ".":
|
||||||
|
return
|
||||||
|
ignore_entries = [
|
||||||
|
f"{codex_prefix}/*.json",
|
||||||
|
f"{codex_prefix}/*.jsonl",
|
||||||
|
f"{codex_prefix}/*.toml",
|
||||||
|
f"{codex_prefix}/log",
|
||||||
|
f"{codex_prefix}/sessions",
|
||||||
|
]
|
||||||
|
_ensure_gitignore_entries(project_path, ignore_entries)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
if tracker:
|
||||||
|
tracker.error("codex-prompts", str(exc))
|
||||||
|
else:
|
||||||
|
console.print(f"[yellow]Warning: could not sync Codex prompts ({exc})[/yellow]")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def init(
|
def init(
|
||||||
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
|
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
|
||||||
@@ -1125,6 +1243,7 @@ def init(
|
|||||||
|
|
||||||
if selected_ai == "codex":
|
if selected_ai == "codex":
|
||||||
tracker.add("commands", "Ensure workspace commands")
|
tracker.add("commands", "Ensure workspace commands")
|
||||||
|
tracker.add("codex-prompts", "Sync Codex prompts")
|
||||||
|
|
||||||
# Use transient so live tree is replaced by the final static render (avoids duplicate output)
|
# Use transient so live tree is replaced by the final static render (avoids duplicate output)
|
||||||
with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
|
with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
|
||||||
@@ -1140,6 +1259,7 @@ def init(
|
|||||||
# Ensure /commands directory for Codex CLI workspaces only
|
# Ensure /commands directory for Codex CLI workspaces only
|
||||||
if selected_ai == "codex":
|
if selected_ai == "codex":
|
||||||
ensure_workspace_commands(project_path, tracker=tracker)
|
ensure_workspace_commands(project_path, tracker=tracker)
|
||||||
|
sync_codex_prompts(project_path, tracker=tracker)
|
||||||
|
|
||||||
# Ensure scripts are executable (POSIX)
|
# Ensure scripts are executable (POSIX)
|
||||||
ensure_executable_scripts(project_path, tracker=tracker)
|
ensure_executable_scripts(project_path, tracker=tracker)
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ scripts:
|
|||||||
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
|
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
|
||||||
---
|
---
|
||||||
|
|
||||||
Given the feature description provided as an argument, do this:
|
The text the user typed after `/specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided a truly empty command.
|
||||||
|
|
||||||
|
Given that feature description, do this:
|
||||||
|
|
||||||
1. Run the script `{SCRIPT}` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute.
|
1. Run the script `{SCRIPT}` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute.
|
||||||
2. Load `templates/spec-template.md` to understand required sections.
|
2. Load `templates/spec-template.md` to understand required sections.
|
||||||
|
|||||||
Reference in New Issue
Block a user