Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
626a3e6497 | ||
|
|
9d56d371a5 | ||
|
|
1d1a4e123b | ||
|
|
2c3fc5647f | ||
|
|
ad738b3df0 | ||
|
|
65a7331968 |
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh .codex/hooks/session-start.sh 2>/dev/null || sh \"$HOME/.codex/hooks/session-start.sh\" 2>/dev/null || true",
|
||||
"statusMessage": "Loading planning context"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh .codex/hooks/user-prompt-submit.sh 2>/dev/null || sh \"$HOME/.codex/hooks/user-prompt-submit.sh\" 2>/dev/null || true"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .codex/hooks/pre_tool_use.py 2>/dev/null || python3 \"$HOME/.codex/hooks/pre_tool_use.py\" 2>/dev/null || true",
|
||||
"statusMessage": "Checking plan before Bash"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .codex/hooks/post_tool_use.py 2>/dev/null || python3 \"$HOME/.codex/hooks/post_tool_use.py\" 2>/dev/null || true",
|
||||
"statusMessage": "Reviewing Bash against plan"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .codex/hooks/stop.py 2>/dev/null || python3 \"$HOME/.codex/hooks/stop.py\" 2>/dev/null || true",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
HOOK_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load_payload() -> dict[str, Any]:
|
||||
raw = sys.stdin.read().strip()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def cwd_from_payload(payload: dict[str, Any]) -> Path:
|
||||
cwd = payload.get("cwd")
|
||||
if isinstance(cwd, str) and cwd:
|
||||
return Path(cwd)
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def emit_json(payload: dict[str, Any]) -> None:
|
||||
if not payload:
|
||||
return
|
||||
json.dump(payload, sys.stdout, ensure_ascii=False)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
|
||||
def parse_json(text: str) -> dict[str, Any]:
|
||||
if not text.strip():
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def run_shell_script(script_name: str, cwd: Path) -> tuple[str, str]:
|
||||
result = subprocess.run(
|
||||
["sh", str(HOOK_DIR / script_name)],
|
||||
cwd=str(cwd),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
return result.stdout.strip(), result.stderr.strip()
|
||||
|
||||
|
||||
def main_guard(func) -> int:
|
||||
try:
|
||||
func()
|
||||
except Exception as exc: # pragma: no cover
|
||||
print(f"[planning-with-files hook] {exc}", file=sys.stderr)
|
||||
return 0
|
||||
return 0
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# planning-with-files: Post-tool-use hook for Codex
|
||||
# Reused from the Cursor integration.
|
||||
|
||||
if [ -f task_plan.md ]; then
|
||||
echo "[planning-with-files] Update progress.md with what you just did. If a phase is now complete, update task_plan.md status."
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import codex_hook_adapter as adapter
|
||||
|
||||
|
||||
def main() -> None:
|
||||
payload = adapter.load_payload()
|
||||
root = adapter.cwd_from_payload(payload)
|
||||
stdout, _ = adapter.run_shell_script("post-tool-use.sh", root)
|
||||
if stdout:
|
||||
adapter.emit_json({"systemMessage": stdout})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(adapter.main_guard(main))
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# planning-with-files: Pre-tool-use hook for Codex
|
||||
# Reused from the Cursor integration.
|
||||
|
||||
PLAN_FILE="task_plan.md"
|
||||
|
||||
if [ -f "$PLAN_FILE" ]; then
|
||||
# Log plan context to stderr so the Codex adapter can surface it as systemMessage.
|
||||
head -30 "$PLAN_FILE" >&2
|
||||
fi
|
||||
|
||||
echo '{"decision": "allow"}'
|
||||
exit 0
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import codex_hook_adapter as adapter
|
||||
|
||||
|
||||
def main() -> None:
|
||||
payload = adapter.load_payload()
|
||||
root = adapter.cwd_from_payload(payload)
|
||||
stdout, stderr = adapter.run_shell_script("pre-tool-use.sh", root)
|
||||
|
||||
result = adapter.parse_json(stdout)
|
||||
decision = result.get("decision")
|
||||
if decision and decision != "allow":
|
||||
adapter.emit_json(result)
|
||||
return
|
||||
|
||||
if stderr:
|
||||
adapter.emit_json({"systemMessage": stderr})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(adapter.main_guard(main))
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# planning-with-files: SessionStart hook for Codex
|
||||
# Runs session catchup, then reuses the same prompt context hook as UserPromptSubmit.
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
CODEX_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
SKILL_DIR="$CODEX_ROOT/skills/planning-with-files"
|
||||
PYTHON_BIN="${PYTHON_BIN:-$(command -v python3 || command -v python)}"
|
||||
|
||||
if [ -n "$PYTHON_BIN" ] && [ -f "$SKILL_DIR/scripts/session-catchup.py" ]; then
|
||||
"$PYTHON_BIN" "$SKILL_DIR/scripts/session-catchup.py" "$(pwd)"
|
||||
fi
|
||||
|
||||
sh "$SCRIPT_DIR/user-prompt-submit.sh"
|
||||
exit 0
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import codex_hook_adapter as adapter
|
||||
|
||||
|
||||
def main() -> None:
|
||||
payload = adapter.load_payload()
|
||||
root = adapter.cwd_from_payload(payload)
|
||||
stdout, _ = adapter.run_shell_script("stop.sh", root)
|
||||
result = adapter.parse_json(stdout)
|
||||
|
||||
message = result.get("followup_message")
|
||||
if not isinstance(message, str) or not message:
|
||||
return
|
||||
|
||||
if "ALL PHASES COMPLETE" in message:
|
||||
adapter.emit_json({"systemMessage": message})
|
||||
return
|
||||
|
||||
if bool(payload.get("stop_hook_active")):
|
||||
adapter.emit_json({"systemMessage": message})
|
||||
return
|
||||
|
||||
adapter.emit_json({"decision": "block", "reason": message})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(adapter.main_guard(main))
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# planning-with-files: Stop hook for Codex
|
||||
# Reused from the Cursor integration; Codex adapts followup_message separately.
|
||||
|
||||
PLAN_FILE="task_plan.md"
|
||||
|
||||
if [ ! -f "$PLAN_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TOTAL=$(grep -c "### Phase" "$PLAN_FILE" || true)
|
||||
COMPLETE=$(grep -cF "**Status:** complete" "$PLAN_FILE" || true)
|
||||
IN_PROGRESS=$(grep -cF "**Status:** in_progress" "$PLAN_FILE" || true)
|
||||
PENDING=$(grep -cF "**Status:** pending" "$PLAN_FILE" || true)
|
||||
|
||||
if [ "$COMPLETE" -eq 0 ] && [ "$IN_PROGRESS" -eq 0 ] && [ "$PENDING" -eq 0 ]; then
|
||||
COMPLETE=$(grep -c "\[complete\]" "$PLAN_FILE" || true)
|
||||
IN_PROGRESS=$(grep -c "\[in_progress\]" "$PLAN_FILE" || true)
|
||||
PENDING=$(grep -c "\[pending\]" "$PLAN_FILE" || true)
|
||||
fi
|
||||
|
||||
: "${TOTAL:=0}"
|
||||
: "${COMPLETE:=0}"
|
||||
: "${IN_PROGRESS:=0}"
|
||||
: "${PENDING:=0}"
|
||||
|
||||
if [ "$COMPLETE" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
|
||||
echo "{\"followup_message\": \"[planning-with-files] ALL PHASES COMPLETE ($COMPLETE/$TOTAL). If the user has additional work, add new phases to task_plan.md before starting.\"}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "{\"followup_message\": \"[planning-with-files] Task incomplete ($COMPLETE/$TOTAL phases done). Update progress.md, then read task_plan.md and continue working on the remaining phases.\"}"
|
||||
exit 0
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# planning-with-files: User prompt submit hook for Codex
|
||||
# Reused from the Cursor integration.
|
||||
|
||||
if [ -f task_plan.md ]; then
|
||||
echo "[planning-with-files] ACTIVE PLAN — current state:"
|
||||
head -50 task_plan.md
|
||||
echo ""
|
||||
echo "=== recent progress ==="
|
||||
tail -20 progress.md 2>/dev/null
|
||||
echo ""
|
||||
echo "[planning-with-files] Read findings.md for research context. Continue from the current phase."
|
||||
fi
|
||||
exit 0
|
||||
@@ -23,7 +23,7 @@ hooks:
|
||||
- type: command
|
||||
command: "SD=\"${CODEX_SKILL_ROOT:-$HOME/.codex/skills/planning-with-files}/scripts\"; powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"$SD/check-complete.ps1\" 2>/dev/null || sh \"$SD/check-complete.sh\""
|
||||
metadata:
|
||||
version: "2.23.0"
|
||||
version: "2.30.0"
|
||||
---
|
||||
|
||||
# Planning with Files
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
@@ -2,6 +2,26 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.30.0] - 2026-04-03
|
||||
|
||||
### Changed
|
||||
|
||||
- **Migrate from `${CLAUDE_PLUGIN_ROOT}` to `${CLAUDE_SKILL_DIR}`** in main SKILL.md
|
||||
- Stop hook, session-catchup path, and templates path now use `${CLAUDE_SKILL_DIR}` (added in Claude Code v2.1.69)
|
||||
- 3-level fallback chain for backward compatibility: `CLAUDE_SKILL_DIR` -> `CLAUDE_PLUGIN_ROOT` -> hardcoded path
|
||||
- Default fallback path updated from `.claude/plugins/planning-with-files` to `.claude/skills/planning-with-files` (matches `npx skills add` install location)
|
||||
- Fixes path resolution failures on Windows and Linux (related to #32)
|
||||
|
||||
### Restructured
|
||||
|
||||
- **IDE-specific configurations moved to dedicated branches**
|
||||
- Master branch now contains only the core skill, templates, scripts, and docs
|
||||
- Each IDE has its own branch: `ide/cursor`, `ide/copilot`, `ide/gemini`, `ide/kiro`, `ide/codex`, `ide/mastra`, `ide/codebuddy`, `ide/factory`, `ide/opencode`, `ide/continue`, `ide/pi-agent`
|
||||
- README updated with branch reference table
|
||||
- Reduces master branch clutter and makes per-IDE maintenance independent
|
||||
|
||||
---
|
||||
|
||||
## [2.29.0] - 2026-03-24
|
||||
|
||||
### Added
|
||||
|
||||
@@ -104,7 +104,7 @@ These IDEs have dedicated hook configurations that automatically re-read your pl
|
||||
| Mastra Code | [Mastra Setup](docs/mastra.md) | Skills + [Hooks](https://mastra.ai/docs/mastra-code/configuration) |
|
||||
| Gemini CLI | [Gemini Setup](docs/gemini.md) | Skills + [Hooks](https://geminicli.com/docs/hooks/) |
|
||||
| Kiro | [Kiro Setup](docs/kiro.md) | [Agent Skills](https://kiro.dev/docs/skills/) |
|
||||
| Codex | [Codex Setup](docs/codex.md) | [Skills + Hooks](https://developers.openai.com/codex/skills) |
|
||||
| Codex | [Codex Setup](docs/codex.md) | [Skills + Hooks](https://developers.openai.com/codex/hooks) |
|
||||
| CodeBuddy | [CodeBuddy Setup](docs/codebuddy.md) | [Skills + Hooks](https://www.codebuddy.ai/docs/cli/skills) |
|
||||
| FactoryAI Droid | [Factory Setup](docs/factory.md) | [Skills + Hooks](https://docs.factory.ai/cli/configuration/skills) |
|
||||
| OpenCode | [OpenCode Setup](docs/opencode.md) | Skills + Custom session storage |
|
||||
@@ -154,7 +154,7 @@ A Claude Code plugin that transforms your workflow to use persistent markdown fi
|
||||
[](https://docs.github.com/en/copilot/reference/hooks-configuration)
|
||||
[](https://code.mastra.ai)
|
||||
[](https://boxlite.ai)
|
||||
[](https://github.com/OthmanAdi/planning-with-files/releases)
|
||||
[](https://github.com/OthmanAdi/planning-with-files/releases)
|
||||
[](https://getskillcheck.com)
|
||||
|
||||
## Quick Install
|
||||
@@ -353,6 +353,8 @@ planning-with-files/
|
||||
│ └── skills/
|
||||
│ └── planning-with-files/
|
||||
├── .codex/ # Codex CLI skills + hooks
|
||||
│ ├── hooks.json # Hook configuration
|
||||
│ ├── hooks/ # Hook scripts + Codex adapters
|
||||
│ └── skills/
|
||||
├── .opencode/ # OpenCode skills (custom session storage)
|
||||
│ └── skills/
|
||||
|
||||
+127
-32
@@ -1,22 +1,35 @@
|
||||
# Codex IDE Support
|
||||
# Codex Setup
|
||||
|
||||
Using planning-with-files with [OpenAI Codex](https://developers.openai.com/codex/).
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
planning-with-files works with Codex as a personal skill in `~/.codex/skills/`.
|
||||
Codex discovers skills from `.codex/skills/` and hooks from `.codex/hooks.json` or `~/.codex/hooks.json`.
|
||||
|
||||
This integration includes both:
|
||||
|
||||
- `.codex/skills/planning-with-files/` for the skill itself
|
||||
- `.codex/hooks.json` plus `.codex/hooks/` for lifecycle automation
|
||||
|
||||
The hook behavior reuses the same mature shell scripts as the Cursor integration, with a thin Codex adapter layer for the differences in hook protocol.
|
||||
|
||||
> **Important:** Codex hooks require `codex_hooks = true` in `~/.codex/config.toml`.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Codex auto-discovers skills from `.codex/skills/` directories. Two installation methods:
|
||||
|
||||
### Method 1: Workspace Installation (Recommended)
|
||||
|
||||
Share the skill with your entire team by adding it to your repository:
|
||||
Share the skill and hooks with your whole team by committing `.codex/` to your repository:
|
||||
|
||||
```bash
|
||||
# In your project repository
|
||||
git clone https://github.com/OthmanAdi/planning-with-files.git /tmp/planning-with-files
|
||||
|
||||
# Copy the Codex skill to your repo
|
||||
# Copy the Codex integration to your repo
|
||||
cp -r /tmp/planning-with-files/.codex .
|
||||
|
||||
# Commit to share with team
|
||||
@@ -28,8 +41,6 @@ git push
|
||||
rm -rf /tmp/planning-with-files
|
||||
```
|
||||
|
||||
Now everyone on your team using Codex will have access to the skill!
|
||||
|
||||
### Method 2: Personal Installation
|
||||
|
||||
Install just for yourself:
|
||||
@@ -38,45 +49,129 @@ Install just for yourself:
|
||||
# Clone the repo
|
||||
git clone https://github.com/OthmanAdi/planning-with-files.git /tmp/planning-with-files
|
||||
|
||||
# Copy to your personal Codex skills folder
|
||||
# Copy the skill
|
||||
mkdir -p ~/.codex/skills
|
||||
cp -r /tmp/planning-with-files/.codex/skills/planning-with-files ~/.codex/skills/
|
||||
|
||||
# Copy the hook scripts
|
||||
mkdir -p ~/.codex/hooks
|
||||
cp -r /tmp/planning-with-files/.codex/hooks/* ~/.codex/hooks/
|
||||
|
||||
# Copy hooks.json
|
||||
# If you already have ~/.codex/hooks.json, merge the planning-with-files entries manually
|
||||
cp /tmp/planning-with-files/.codex/hooks.json ~/.codex/hooks.json
|
||||
|
||||
# Clean up
|
||||
rm -rf /tmp/planning-with-files
|
||||
```
|
||||
|
||||
## Usage with Superpowers
|
||||
> **Note:** If you already have a `~/.codex/hooks.json`, do not overwrite it blindly. Merge the `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` entries into your existing file.
|
||||
|
||||
If you have [obra/superpowers](https://github.com/obra/superpowers) installed:
|
||||
### Enable Hooks in `config.toml`
|
||||
|
||||
```bash
|
||||
~/.codex/superpowers/.codex/superpowers-codex use-skill planning-with-files
|
||||
Ensure your `~/.codex/config.toml` contains:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
codex_hooks = true
|
||||
```
|
||||
|
||||
## Usage without Superpowers
|
||||
If you already have a `[features]` section, add `codex_hooks = true` under it instead of creating a duplicate section.
|
||||
|
||||
Add to your `~/.codex/AGENTS.md`:
|
||||
|
||||
```markdown
|
||||
## Planning with Files
|
||||
|
||||
<IMPORTANT>
|
||||
For complex tasks (3+ steps, research, projects):
|
||||
1. Read skill: `cat ~/.codex/skills/planning-with-files/SKILL.md`
|
||||
2. Create task_plan.md, findings.md, progress.md in your project directory
|
||||
3. Follow 3-file pattern throughout the task
|
||||
</IMPORTANT>
|
||||
```
|
||||
|
||||
## Verification
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
codex --version
|
||||
codex features list | rg '^codex_hooks\\s'
|
||||
ls -la ~/.codex/skills/planning-with-files/SKILL.md
|
||||
ls -la ~/.codex/hooks.json ~/.codex/hooks/
|
||||
```
|
||||
|
||||
## Learn More
|
||||
If `codex_hooks` does not appear in `codex features list`, upgrade Codex before troubleshooting the skill.
|
||||
|
||||
- [Installation Guide](installation.md)
|
||||
- [Quick Start](quickstart.md)
|
||||
- [Workflow Diagram](workflow.md)
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### Hooks
|
||||
|
||||
Codex reads hooks from:
|
||||
|
||||
1. `.codex/hooks.json` in your project root
|
||||
2. `~/.codex/hooks.json` for your global install
|
||||
|
||||
This integration includes all five Codex lifecycle hooks:
|
||||
|
||||
| Hook | What It Does |
|
||||
|------|--------------|
|
||||
| **SessionStart** | Runs `session-catchup.py`, then injects active plan context |
|
||||
| **UserPromptSubmit** | Re-injects plan and recent progress on every user message |
|
||||
| **PreToolUse** | Re-reads the first 30 lines of `task_plan.md` before Bash |
|
||||
| **PostToolUse** | Reminds the agent to update `progress.md` after Bash activity |
|
||||
| **Stop** | Blocks once when phases are incomplete, then falls back to a follow-up reminder |
|
||||
|
||||
### The Three Files
|
||||
|
||||
Once activated, the skill creates and maintains:
|
||||
|
||||
| File | Purpose | Location |
|
||||
|------|---------|----------|
|
||||
| `task_plan.md` | Phases, progress, decisions | Your project root |
|
||||
| `findings.md` | Research, discoveries | Your project root |
|
||||
| `progress.md` | Session log, test results | Your project root |
|
||||
|
||||
---
|
||||
|
||||
## Team Workflow
|
||||
|
||||
### Workspace Installation
|
||||
|
||||
With workspace installation (`.codex/` committed to your repo):
|
||||
|
||||
- Everyone on the team gets the same skill and hooks
|
||||
- The Codex setup is version controlled with the project
|
||||
- Updates ship through normal git review
|
||||
|
||||
### Personal Installation
|
||||
|
||||
With personal installation (`~/.codex/`):
|
||||
|
||||
- You can use the skill across all projects
|
||||
- You keep your setup even if you change repositories
|
||||
- Existing global hooks may need manual merging
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hooks Not Running?
|
||||
|
||||
1. Check that `codex_hooks = true` is present in `~/.codex/config.toml`
|
||||
2. Verify `.codex/hooks.json` or `~/.codex/hooks.json` exists
|
||||
3. Restart Codex after adding or changing hooks
|
||||
4. Run `codex features list | rg '^codex_hooks\\s'`
|
||||
|
||||
### Already Using Other Global Hooks?
|
||||
|
||||
That is fine, but do not overwrite your existing `~/.codex/hooks.json`. Merge the planning-with-files entries instead.
|
||||
|
||||
### Seeing Duplicate Hook Messages?
|
||||
|
||||
Avoid installing the same planning-with-files hooks in both places at once:
|
||||
|
||||
- workspace `.codex/hooks.json`
|
||||
- global `~/.codex/hooks.json`
|
||||
|
||||
If you enable both, Codex may run both sets of hooks and duplicate the reminders.
|
||||
|
||||
### Windows Support
|
||||
|
||||
OpenAI's current Codex hooks documentation says hooks are disabled on Windows. The skill files can still be installed there, but the hook automation is currently for macOS/Linux Codex environments.
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **GitHub Issues:** https://github.com/OthmanAdi/planning-with-files/issues
|
||||
- **OpenAI Codex Hooks Docs:** https://developers.openai.com/codex/hooks
|
||||
- **OpenAI Codex Skills Docs:** https://developers.openai.com/codex/skills
|
||||
|
||||
@@ -21,9 +21,9 @@ hooks:
|
||||
Stop:
|
||||
- hooks:
|
||||
- type: command
|
||||
command: "SD=\"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/planning-with-files}/scripts\"; powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"$SD/check-complete.ps1\" 2>/dev/null || sh \"$SD/check-complete.sh\""
|
||||
command: "SD=\"${CLAUDE_SKILL_DIR:-${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/skills/planning-with-files}}/scripts\"; powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"$SD/check-complete.ps1\" 2>/dev/null || sh \"$SD/check-complete.sh\""
|
||||
metadata:
|
||||
version: "2.26.1"
|
||||
version: "2.30.0"
|
||||
---
|
||||
|
||||
# Planning with Files
|
||||
@@ -39,7 +39,7 @@ Work like Manus: Use persistent markdown files as your "working memory on disk."
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
$(command -v python3 || command -v python) ${CLAUDE_PLUGIN_ROOT}/scripts/session-catchup.py "$(pwd)"
|
||||
$(command -v python3 || command -v python) ${CLAUDE_SKILL_DIR}/scripts/session-catchup.py "$(pwd)"
|
||||
```
|
||||
|
||||
```powershell
|
||||
@@ -55,7 +55,7 @@ If catchup report shows unsynced context:
|
||||
|
||||
## Important: Where Files Go
|
||||
|
||||
- **Templates** are in `${CLAUDE_PLUGIN_ROOT}/templates/`
|
||||
- **Templates** are in `${CLAUDE_SKILL_DIR}/templates/`
|
||||
- **Your planning files** go in **your project directory**
|
||||
|
||||
| Location | What Goes There |
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
CODEX_ROOT = REPO_ROOT / ".codex"
|
||||
HOOKS_JSON = CODEX_ROOT / "hooks.json"
|
||||
HOOKS_DIR = CODEX_ROOT / "hooks"
|
||||
|
||||
|
||||
class CodexHooksTests(unittest.TestCase):
|
||||
def run_python_hook(self, script_name: str, payload: dict, cwd: Path) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(HOOKS_DIR / script_name)],
|
||||
input=json.dumps(payload),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
cwd=str(cwd),
|
||||
check=False,
|
||||
)
|
||||
|
||||
def run_shell_hook(self, script_name: str, cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["sh", str(HOOKS_DIR / script_name)],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
cwd=str(cwd),
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_hooks_json_declares_all_expected_codex_events(self) -> None:
|
||||
self.assertTrue(HOOKS_JSON.exists(), ".codex/hooks.json is missing")
|
||||
|
||||
payload = json.loads(HOOKS_JSON.read_text(encoding="utf-8"))
|
||||
self.assertEqual(
|
||||
{"SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"},
|
||||
set(payload["hooks"]),
|
||||
)
|
||||
|
||||
def test_session_start_reuses_plan_context(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir, tempfile.TemporaryDirectory() as home:
|
||||
root = Path(tmpdir)
|
||||
root.joinpath("task_plan.md").write_text(
|
||||
"# Task Plan\n\n## Goal\nShip Codex hooks\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
root.joinpath("progress.md").write_text(
|
||||
"# Progress\n\nFinished adapter draft.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
root.joinpath("findings.md").write_text(
|
||||
"# Findings\n\n- reuse cursor hooks\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = home
|
||||
result = self.run_shell_hook("session-start.sh", root, env=env)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("ACTIVE PLAN", result.stdout)
|
||||
self.assertIn("Ship Codex hooks", result.stdout)
|
||||
self.assertIn("Finished adapter draft", result.stdout)
|
||||
|
||||
def test_pre_tool_use_adapter_emits_system_message(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
root.joinpath("task_plan.md").write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
# Task Plan
|
||||
### Phase 1: Discovery
|
||||
- **Status:** complete
|
||||
"""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_python_hook(
|
||||
"pre_tool_use.py",
|
||||
{"cwd": str(root), "tool_input": {"command": "pwd"}},
|
||||
root,
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
payload = json.loads(result.stdout)
|
||||
self.assertIn("systemMessage", payload)
|
||||
self.assertIn("# Task Plan", payload["systemMessage"])
|
||||
|
||||
def test_post_tool_use_adapter_emits_progress_reminder(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
root.joinpath("task_plan.md").write_text("# Task Plan\n", encoding="utf-8")
|
||||
|
||||
result = self.run_python_hook(
|
||||
"post_tool_use.py",
|
||||
{"cwd": str(root), "tool_response": "ok"},
|
||||
root,
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
payload = json.loads(result.stdout)
|
||||
self.assertIn("progress.md", payload["systemMessage"])
|
||||
|
||||
def test_stop_adapter_blocks_once_then_allows_reentry(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
root.joinpath("task_plan.md").write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
### Phase 1: Discovery
|
||||
- **Status:** complete
|
||||
|
||||
### Phase 2: Implementation
|
||||
- **Status:** pending
|
||||
"""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
first = self.run_python_hook(
|
||||
"stop.py",
|
||||
{"cwd": str(root), "stop_hook_active": False},
|
||||
root,
|
||||
)
|
||||
second = self.run_python_hook(
|
||||
"stop.py",
|
||||
{"cwd": str(root), "stop_hook_active": True},
|
||||
root,
|
||||
)
|
||||
|
||||
self.assertEqual(0, first.returncode, first.stderr)
|
||||
self.assertEqual(0, second.returncode, second.stderr)
|
||||
|
||||
first_payload = json.loads(first.stdout)
|
||||
second_payload = json.loads(second.stdout)
|
||||
|
||||
self.assertEqual("block", first_payload["decision"])
|
||||
self.assertIn("Task incomplete", first_payload["reason"])
|
||||
self.assertIn("Task incomplete", second_payload["systemMessage"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
import stat
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT_PATHS = [
|
||||
".codex/skills/planning-with-files/scripts/init-session.sh",
|
||||
".codex/skills/planning-with-files/scripts/check-complete.sh",
|
||||
".codex/skills/planning-with-files/scripts/session-catchup.py",
|
||||
]
|
||||
|
||||
|
||||
class CodexScriptPermissionTests(unittest.TestCase):
|
||||
@unittest.skipIf(os.name == "nt", "Executable mode bits are not reliable on Windows.")
|
||||
def test_codex_direct_run_scripts_are_tracked_as_executable(self) -> None:
|
||||
for path in SCRIPT_PATHS:
|
||||
with self.subTest(path=path):
|
||||
mode = (REPO_ROOT / path).stat().st_mode
|
||||
self.assertTrue(mode & stat.S_IXUSR, f"{path} is missing user execute bit")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user