Compare commits

...
6 Commits
Author SHA1 Message Date
Ahmad Othman Ammar Adi. 626a3e6497 fix(codex): add official hooks.json integration
fix(codex): add official hooks.json integration
2026-04-06 13:00:15 +02:00
leon 9d56d371a5 fix(codex): add official hooks.json integration 2026-04-06 15:56:25 +08:00
Ahmad Othman Ammar Adi. 1d1a4e123b Merge pull request #119 from Leon-Algo/codex/codex-script-exec
fix(codex): make planning scripts executable in .codex skill install
2026-04-05 21:29:57 +02:00
leon 2c3fc5647f fix(codex): make planning scripts executable in .codex skill install 2026-04-05 22:00:29 +08:00
OthmanAdi ad738b3df0 chore(codex): bump SKILL.md version to 2.30.0 (was 2.23.0) 2026-04-03 01:32:19 +02:00
OthmanAdi 65a7331968 fix(skill): migrate from CLAUDE_PLUGIN_ROOT to CLAUDE_SKILL_DIR
Replace ${CLAUDE_PLUGIN_ROOT} with ${CLAUDE_SKILL_DIR} in main SKILL.md
for reliable path resolution across all install methods.

- Stop hook: 3-level fallback (CLAUDE_SKILL_DIR -> CLAUDE_PLUGIN_ROOT -> hardcoded)
- Session catchup: uses CLAUDE_SKILL_DIR for script path
- Templates: uses CLAUDE_SKILL_DIR for template path
- Default fallback updated to .claude/skills/ (npx skills add location)
- Bump version to 2.30.0

Related: #32
2026-04-03 01:27:03 +02:00
20 changed files with 610 additions and 39 deletions
+61
View File
@@ -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
}
]
}
]
}
}
+66
View File
@@ -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
+8
View File
@@ -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
+16
View File
@@ -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))
+13
View File
@@ -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
+23
View File
@@ -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))
+15
View File
@@ -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
+29
View File
@@ -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))
+33
View File
@@ -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
+14
View File
@@ -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
+1 -1
View File
@@ -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
View File
View File
View File
+20
View File
@@ -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
+4 -2
View File
@@ -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
[![GitHub Copilot](https://img.shields.io/badge/GitHub%20Copilot-Hooks-000000)](https://docs.github.com/en/copilot/reference/hooks-configuration)
[![Mastra Code](https://img.shields.io/badge/Mastra%20Code-Skills-00BCD4)](https://code.mastra.ai)
[![BoxLite](https://img.shields.io/badge/BoxLite-Sandbox-6C3483)](https://boxlite.ai)
[![Version](https://img.shields.io/badge/version-2.29.0-brightgreen)](https://github.com/OthmanAdi/planning-with-files/releases)
[![Version](https://img.shields.io/badge/version-2.30.0-brightgreen)](https://github.com/OthmanAdi/planning-with-files/releases)
[![SkillCheck Validated](https://img.shields.io/badge/SkillCheck-Validated-4c1)](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
View File
@@ -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
+4 -4
View File
@@ -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 |
+151
View File
@@ -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()
+25
View File
@@ -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()