Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9cdc84d
Add --commit-plan-file option for plan file version control
SihaoLiu Jan 14, 2026
d977d3b
Add validation for plan file outside project with --commit-plan-file
SihaoLiu Jan 14, 2026
a05f080
Fix PROJECT_ROOT unbound variable and improve plan file modification …
SihaoLiu Jan 15, 2026
8fc5d01
Update version bump rules for feature branches vs main branch
SihaoLiu Jan 15, 2026
425b630
Fix plan file handling for all four cases (P1, P2, P3)
SihaoLiu Jan 15, 2026
a86bc04
Fix stop hook to follow Claude Code hooks spec
SihaoLiu Jan 15, 2026
9bc8214
Simplify plan file handling code
SihaoLiu Jan 15, 2026
9555c5c
Fix plan file handling edge cases (P1-P4)
SihaoLiu Jan 15, 2026
f112744
Move plan file validation to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
6d02655
Move pre-1.1.2 state file check to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
42009ed
Fix pre-1.1.2 loop handling and portable path resolution
SihaoLiu Jan 15, 2026
34da7fa
Fix Python fallback injection, test signing, and README Case 4
SihaoLiu Jan 15, 2026
bc0fb24
Improve get_relative_path to handle paths outside base directory
SihaoLiu Jan 15, 2026
6a18c15
Add pre-commit check to block git commit when plan file is staged
SihaoLiu Jan 15, 2026
5dc3a6a
Fix --commit-plan-file validation for monorepo subdirectories
SihaoLiu Jan 15, 2026
54147e1
Fix plan validator inside/outside repo check for monorepo subdirectories
SihaoLiu Jan 15, 2026
1706bbd
Fix git status/diff path matching for monorepo subdirectories
SihaoLiu Jan 15, 2026
eb32366
Fix regex escaping for plan file paths with + character
SihaoLiu Jan 15, 2026
00930e9
Add git ancestry check and prefixed state file termination
SihaoLiu Jan 15, 2026
9c075fe
Fix git commands to use git-relative path in plan validator
SihaoLiu Jan 15, 2026
4dd1ca2
Fix code review issues for monorepo and POSIX compatibility
SihaoLiu Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@
"name": "humanize",
"source": "./",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1"
"version": "1.1.2"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"name": "humanize",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1",
"version": "1.1.2",
"author": {
"name": "humania-org"
},
Expand Down
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ This is a Claude Code plugin that provides iterative development with Codex revi

# Humanize Project Rules
- Everything about this project, including but not limited to implementations, comments, tests and documentations should be in English. No Emoji or CJK char is allowed.
- **MANDATORY**: Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). This applies to ALL commits without exception - bug fixes, features, documentation changes, etc. Increment the patch version (e.g., 1.0.0 -> 1.0.1) for each commit.
- **Version Update Rules**:
- **On feature branches** (not `main` or `master`): No need to bump version on every commit. However, ensure the branch has an incremental version update compared to the default main branch before merging.
- **On default branch** (`main` or `master`): Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). Increment the patch version (e.g., 1.0.0 -> 1.0.1, 1.0.9 -> 1.1.0, 3.4.7 -> 4.0.0) for each commit.
- Every `git push` or `git commit` MUST confirm with user first, MUST NOT commit or push to remote without check with user.
- Version number must be in format of `X.Y.Z` where X/Y/Z is numeric number. Version MUST NOT include anything other than `X.Y.Z`. For example, a good version is `9.732.42`; Bad version examples (MUST NOT USE): `3.22.7-alpha` (extra "-alpha" string), `9.77.2 (2026-01-07)` (useless date/timestamp).
10 changes: 10 additions & 0 deletions .github/workflows/template-test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,13 +6,15 @@ on:
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'
pull_request:
branches: ['**']
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'

Expand DownExpand Up@@ -47,6 +49,14 @@ jobs:
chmod +x tests/test-template-references.sh
./tests/test-template-references.sh

- name: Run plan file handling tests
run: |
echo "========================================"
echo "Running plan file handling tests"
echo "========================================"
chmod +x tests/test-plan-file-handling.sh
./tests/test-plan-file-handling.sh

- name: Verify all hook scripts still work
run: |
echo "========================================"
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.1**
**Current Version: 1.1.2**

> Derived from the [GAAC (GitHub-as-a-Context)](https://github.com/SihaoLiu/gaac) project.

Expand DownExpand Up@@ -115,18 +115,26 @@ This provides a real-time dashboard showing:

- **Loop state**: Controlled solely by the presence of `.humanize-loop.local/*/state.md`
- **Resume**: Simply restart Claude Code in the same directory - the loop continues automatically
- **Cancel**: Remove the state file to stop the loop permanently
- **Cancel**: Rename the state file with a prefix to stop the loop

```bash
# Cancel the active loop
/humanize:cancel-rlcr-loop

# Or manually remove state file
rm .humanize-loop.local/*/state.md
# Or manually rename state file
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```

The loop directory with all summaries and review results is preserved for reference.

**State file prefixes indicate termination reason:**
- `completed-state.md` - Normal completion (Codex said COMPLETE)
- `stopped-state.md` - Stagnation/circuit breaker (Codex said STOP, or max iterations)
- `cancelled-state.md` - User manually cancelled
- `unexpected-state.md` - Error conditions (corruption, legacy state, branch change)

Any prefixed state file can be renamed back to `state.md` to manually restart the loop.

## Goal Tracker System

Humanize uses a **Goal Tracker** to prevent goal drift across iterations:
Expand DownExpand Up@@ -196,19 +204,53 @@ OPTIONS:
--codex-timeout <SECONDS>
Timeout for each Codex review in seconds (default: 5400)
--push-every-round Require git push after each round (default: commits stay local)
--commit-plan-file Include the plan file in commits (default: plan file stays uncommitted)
-h, --help Show help message
```

### Plan File Handling

The plan file behavior depends on the `--commit-plan-file` flag and whether the plan file is inside or outside the git repository:

#### Case 1: Inside repo with `--commit-plan-file`
- **Setup**: Plan file must be tracked (committed) AND clean (no uncommitted changes)
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing
- **Stop hook**: If plan file has uncommitted changes (dirty), allows stop with error message
- Plan file changes must be committed like any other file

#### Case 2: Outside repo with `--commit-plan-file`
- **Setup**: Early failure - this configuration is not allowed
- **Stop hook**: If detected (e.g., from manual state file edit), allows stop with conflict error

#### Case 3: Inside repo without `--commit-plan-file` (default)
- **Setup**: Plan file can be tracked or untracked, dirty or clean
- **UserPromptSubmit hook**: If plan file content differs from backup (or is missing), **blocks prompt** before processing with recovery options
- Accidental commits of the plan file are blocked with an error
- A backup of the original plan file is saved to `.humanize-loop.local/<timestamp>/plan-backup.md`

#### Case 4: Outside repo without `--commit-plan-file`
- **Setup**: No restrictions
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing with recovery options

**Key principle**: Plan file content changes are detected **before prompt processing** via the UserPromptSubmit hook. This blocks work from starting with a stale plan, giving the user a chance to restart the loop or restore the original plan.

## Prerequisites

Required tools:
**Required environment:**
- Git repository with at least one commit
- Plan file path must not contain spaces or special characters (`[ ] * ? { } | ( ) ^ $ \`)

**Required tools:**
- `codex` - OpenAI Codex CLI (for review)

Check if Codex is available:
```bash
codex --version
```

**Note on upgrading from pre-1.1.2:**
If you have an active RLCR loop started with a version before 1.1.2, the loop will be automatically terminated on the next stop attempt. Your work is preserved in `.humanize-loop.local/` - simply start a new loop with the updated plugin.

## Directory Structure

```
Expand DownExpand Up@@ -243,7 +285,8 @@ humanize/
When loop is active, creates: `.humanize-loop.local/<TIMESTAMP>/`

**Files Created**:
- `state.md` - Current round, config (YAML frontmatter)
- `state.md` - Current round, config (YAML frontmatter with `start_commit`, `commit_plan_file`, etc.)
- `plan-backup.md` - Backup copy of the original plan file at loop start
- `goal-tracker.md` - Immutable (goals/AC) + Mutable (active tasks, deferred, etc.)
- `round-N-prompt.md` - Instructions FROM Codex TO Claude
- `round-N-summary.md` - Work summary written BY Claude
Expand Down
8 changes: 6 additions & 2 deletions commands/cancel-rlcr-loop.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
description: "Cancel active RLCR loop"
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(rm .humanize-loop.local/*/state.md)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(for f in .humanize-loop.local/*/state.md*)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
hide-from-slash-command-tool: "true"
---

Expand All@@ -18,7 +18,11 @@ ls .humanize-loop.local/*/state.md 2>/dev/null || echo "NO_LOOP"

3. **If state file(s) found**:
- Read the state file to get the current round number
- Remove the state file(s) using: `rm .humanize-loop.local/*/state.md`
- Rename the state file(s) to cancelled-state.md:
```bash
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```
- Report: "Cancelled RLCR loop (was at round N of M)"

The loop directory with summaries and review results will be preserved for reference.
The cancelled-state.md file can be renamed back to state.md to manually restart the loop if needed.
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
{
"description": "Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-validator.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write",
Expand Down
169 changes: 169 additions & 0 deletions hooks/lib/loop-common.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,46 @@ if ! validate_template_dir "$TEMPLATE_DIR" 2>/dev/null; then
echo "Warning: Template directory validation failed. Using inline fallbacks." >&2
fi

# Stop an active loop by renaming state.md to <prefix>-state.md
# This preserves the state file for manual inspection/restart if needed
# The prefix indicates why the loop was stopped:
# - completed: Normal completion (Codex said COMPLETE)
# - stopped: Stagnation/circuit breaker (Codex said STOP, or max iterations)
# - unexpected: Error conditions (corruption, legacy state, branch change)
# - cancelled: User manually cancelled
# Usage: stop_loop "$STATE_FILE" "completed|stopped|unexpected|cancelled"
stop_loop() {
local state_file="$1"
local prefix="${2:-stopped}" # Default to "stopped" for backward compatibility
if [[ -f "$state_file" ]]; then
local new_file="${state_file%state.md}${prefix}-state.md"
mv "$state_file" "$new_file" 2>/dev/null || rm -f "$state_file"
fi
}

# Check if HEAD is a descendant of start_commit
# Returns 0 if HEAD is descendant (valid), 1 if not (user checked out older branch)
# Usage: check_start_commit_ancestry "$START_COMMIT"
check_start_commit_ancestry() {
local start_commit="$1"

# If no start_commit or not in a git repo, skip check
if [[ -z "$start_commit" ]]; then
return 0
fi

if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
return 0
fi

# Check if start_commit is an ancestor of HEAD
if git merge-base --is-ancestor "$start_commit" HEAD 2>/dev/null; then
return 0
else
return 1
fi
}

# Find the most recent active loop directory
# Only checks the newest directory - older directories are ignored even if they have state.md
# This prevents "zombie" loops from being revived after abnormal exits
Expand DownExpand Up@@ -62,6 +102,135 @@ to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# Normalize a path: remove double slashes, resolve . and .. components
# Usage: _normalize_path "/home//user/../project/./src"
# Output: "/home/project/src"
# Note: This is a helper for get_relative_path, defined at top level for zsh compatibility
_normalize_path() {
# Enable shwordsplit for zsh (localoptions makes it local to this function)
[[ -n "$ZSH_VERSION" ]] && setopt localoptions shwordsplit

local path="$1"
# Remove trailing slash
path="${path%/}"
# Remove double slashes
while [[ "$path" == *//* ]]; do
path="${path//\/\//\/}"
done
# Resolve . and .. components by iterating over path parts
local result="" part old_ifs="$IFS"
IFS='/'
set -f # Disable glob expansion
for part in ${path#/}; do
if [[ -z "$part" ]] || [[ "$part" == "." ]]; then
continue
elif [[ "$part" == ".." ]]; then
# Remove last component from result
result="${result%/*}"
else
result="$result/$part"
fi
done
set +f # Re-enable glob expansion
IFS="$old_ifs"
if [[ -z "$result" ]]; then
echo "/"
else
echo "$result"
fi
}

# Compute relative path from base to target (portable across Linux/macOS/BSD)
# Usage: get_relative_path "/base/dir" "/base/dir/sub/file.txt"
# Output: "sub/file.txt"
get_relative_path() {
# Enable 0-indexed arrays in zsh for bash compatibility
[[ -n "$ZSH_VERSION" ]] && setopt localoptions ksharrays

local base="$1"
local target="$2"

# Normalize paths (resolve symlinks, remove trailing slashes)
local base_real target_real
base_real=$(cd "$base" 2>/dev/null && pwd -P) || base_real="$base"
target_real=$(realpath "$target" 2>/dev/null) || target_real="$target"

# Try GNU realpath --relative-to (Linux)
if realpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi

# Try grealpath (macOS with coreutils)
if command -v grealpath &>/dev/null; then
if grealpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi
fi

# Fallback: compute relative path in bash/zsh (works for inside/outside base)
if [[ "$base_real" == /* ]] && [[ "$target_real" == /* ]]; then
local base_clean target_clean

# Normalize paths using top-level helper (for zsh compatibility)
base_clean=$(_normalize_path "$base_real")
target_clean=$(_normalize_path "$target_real")
[[ -z "$base_clean" ]] && base_clean="/"
[[ -z "$target_clean" ]] && target_clean="/"

if [[ "$base_clean" == "$target_clean" ]]; then
echo "."
return 0
fi

local base_trim target_trim
base_trim="${base_clean#/}"
target_trim="${target_clean#/}"

# Split paths into arrays (bash/zsh compatible using awk)
# This avoids read -a which is bash-specific
local base_parts target_parts rel_parts
base_parts=$(echo "$base_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')
target_parts=$(echo "$target_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')

# Convert to arrays using while read (portable)
local -a base_arr target_arr rel_arr
while IFS= read -r part; do
[[ -n "$part" ]] && base_arr+=("$part")
done <<< "$base_parts"
while IFS= read -r part; do
[[ -n "$part" ]] && target_arr+=("$part")
done <<< "$target_parts"

# Find common prefix length
local i=0
while [[ $i -lt ${#base_arr[@]} ]] && [[ $i -lt ${#target_arr[@]} ]] && \
[[ "${base_arr[$i]}" == "${target_arr[$i]}" ]]; do
((i++))
done

# Add ".." for each remaining base component
local j
for ((j=i; j<${#base_arr[@]}; j++)); do
rel_arr+=("..")
done
# Add remaining target components
for ((j=i; j<${#target_arr[@]}; j++)); do
rel_arr+=("${target_arr[$j]}")
done

if (( ${#rel_arr[@]} == 0 )); then
echo "."
else
local IFS='/'
echo "${rel_arr[*]}"
fi
return 0
fi

# Cannot compute relative path, return basename as last resort
basename "$target"
Comment on lines +229 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect external plan files correctly in get_relative_path

On macOS (or any environment without GNU realpath --relative-to and without grealpath), get_relative_path falls back to basename when the target isn’t under the base directory. Downstream logic in setup-rlcr-loop.sh and loop-plan-validator.sh treats “outside repo” as PLAN_FILE_REL starting with ../, so returning just the basename misclassifies an external plan file as inside the repo. That means the --commit-plan-file outside-repo conflict won’t trigger, and it can even incorrectly “match” a tracked file at the repo root with the same basename. This breaks the intended Case 3 behavior on macOS. Consider returning a true relative path (e.g., via python -c 'os.path.relpath') or an absolute path and updating the inside/outside check accordingly.

Useful? React with 👍 / 👎.

}

# Check if a path (lowercase) matches a round file pattern
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos"
is_round_file_type() {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9cdc84d
Add --commit-plan-file option for plan file version control
SihaoLiu Jan 14, 2026
d977d3b
Add validation for plan file outside project with --commit-plan-file
SihaoLiu Jan 14, 2026
a05f080
Fix PROJECT_ROOT unbound variable and improve plan file modification …
SihaoLiu Jan 15, 2026
8fc5d01
Update version bump rules for feature branches vs main branch
SihaoLiu Jan 15, 2026
425b630
Fix plan file handling for all four cases (P1, P2, P3)
SihaoLiu Jan 15, 2026
a86bc04
Fix stop hook to follow Claude Code hooks spec
SihaoLiu Jan 15, 2026
9bc8214
Simplify plan file handling code
SihaoLiu Jan 15, 2026
9555c5c
Fix plan file handling edge cases (P1-P4)
SihaoLiu Jan 15, 2026
f112744
Move plan file validation to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
6d02655
Move pre-1.1.2 state file check to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
42009ed
Fix pre-1.1.2 loop handling and portable path resolution
SihaoLiu Jan 15, 2026
34da7fa
Fix Python fallback injection, test signing, and README Case 4
SihaoLiu Jan 15, 2026
bc0fb24
Improve get_relative_path to handle paths outside base directory
SihaoLiu Jan 15, 2026
6a18c15
Add pre-commit check to block git commit when plan file is staged
SihaoLiu Jan 15, 2026
5dc3a6a
Fix --commit-plan-file validation for monorepo subdirectories
SihaoLiu Jan 15, 2026
54147e1
Fix plan validator inside/outside repo check for monorepo subdirectories
SihaoLiu Jan 15, 2026
1706bbd
Fix git status/diff path matching for monorepo subdirectories
SihaoLiu Jan 15, 2026
eb32366
Fix regex escaping for plan file paths with + character
SihaoLiu Jan 15, 2026
00930e9
Add git ancestry check and prefixed state file termination
SihaoLiu Jan 15, 2026
9c075fe
Fix git commands to use git-relative path in plan validator
SihaoLiu Jan 15, 2026
4dd1ca2
Fix code review issues for monorepo and POSIX compatibility
SihaoLiu Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@
"name": "humanize",
"source": "./",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1"
"version": "1.1.2"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"name": "humanize",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1",
"version": "1.1.2",
"author": {
"name": "humania-org"
},
Expand Down
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ This is a Claude Code plugin that provides iterative development with Codex revi

# Humanize Project Rules
- Everything about this project, including but not limited to implementations, comments, tests and documentations should be in English. No Emoji or CJK char is allowed.
- **MANDATORY**: Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). This applies to ALL commits without exception - bug fixes, features, documentation changes, etc. Increment the patch version (e.g., 1.0.0 -> 1.0.1) for each commit.
- **Version Update Rules**:
- **On feature branches** (not `main` or `master`): No need to bump version on every commit. However, ensure the branch has an incremental version update compared to the default main branch before merging.
- **On default branch** (`main` or `master`): Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). Increment the patch version (e.g., 1.0.0 -> 1.0.1, 1.0.9 -> 1.1.0, 3.4.7 -> 4.0.0) for each commit.
- Every `git push` or `git commit` MUST confirm with user first, MUST NOT commit or push to remote without check with user.
- Version number must be in format of `X.Y.Z` where X/Y/Z is numeric number. Version MUST NOT include anything other than `X.Y.Z`. For example, a good version is `9.732.42`; Bad version examples (MUST NOT USE): `3.22.7-alpha` (extra "-alpha" string), `9.77.2 (2026-01-07)` (useless date/timestamp).
10 changes: 10 additions & 0 deletions .github/workflows/template-test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,13 +6,15 @@ on:
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'
pull_request:
branches: ['**']
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'

Expand DownExpand Up@@ -47,6 +49,14 @@ jobs:
chmod +x tests/test-template-references.sh
./tests/test-template-references.sh

- name: Run plan file handling tests
run: |
echo "========================================"
echo "Running plan file handling tests"
echo "========================================"
chmod +x tests/test-plan-file-handling.sh
./tests/test-plan-file-handling.sh

- name: Verify all hook scripts still work
run: |
echo "========================================"
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.1**
**Current Version: 1.1.2**

> Derived from the [GAAC (GitHub-as-a-Context)](https://github.com/SihaoLiu/gaac) project.

Expand DownExpand Up@@ -115,18 +115,26 @@ This provides a real-time dashboard showing:

- **Loop state**: Controlled solely by the presence of `.humanize-loop.local/*/state.md`
- **Resume**: Simply restart Claude Code in the same directory - the loop continues automatically
- **Cancel**: Remove the state file to stop the loop permanently
- **Cancel**: Rename the state file with a prefix to stop the loop

```bash
# Cancel the active loop
/humanize:cancel-rlcr-loop

# Or manually remove state file
rm .humanize-loop.local/*/state.md
# Or manually rename state file
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```

The loop directory with all summaries and review results is preserved for reference.

**State file prefixes indicate termination reason:**
- `completed-state.md` - Normal completion (Codex said COMPLETE)
- `stopped-state.md` - Stagnation/circuit breaker (Codex said STOP, or max iterations)
- `cancelled-state.md` - User manually cancelled
- `unexpected-state.md` - Error conditions (corruption, legacy state, branch change)

Any prefixed state file can be renamed back to `state.md` to manually restart the loop.

## Goal Tracker System

Humanize uses a **Goal Tracker** to prevent goal drift across iterations:
Expand DownExpand Up@@ -196,19 +204,53 @@ OPTIONS:
--codex-timeout <SECONDS>
Timeout for each Codex review in seconds (default: 5400)
--push-every-round Require git push after each round (default: commits stay local)
--commit-plan-file Include the plan file in commits (default: plan file stays uncommitted)
-h, --help Show help message
```

### Plan File Handling

The plan file behavior depends on the `--commit-plan-file` flag and whether the plan file is inside or outside the git repository:

#### Case 1: Inside repo with `--commit-plan-file`
- **Setup**: Plan file must be tracked (committed) AND clean (no uncommitted changes)
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing
- **Stop hook**: If plan file has uncommitted changes (dirty), allows stop with error message
- Plan file changes must be committed like any other file

#### Case 2: Outside repo with `--commit-plan-file`
- **Setup**: Early failure - this configuration is not allowed
- **Stop hook**: If detected (e.g., from manual state file edit), allows stop with conflict error

#### Case 3: Inside repo without `--commit-plan-file` (default)
- **Setup**: Plan file can be tracked or untracked, dirty or clean
- **UserPromptSubmit hook**: If plan file content differs from backup (or is missing), **blocks prompt** before processing with recovery options
- Accidental commits of the plan file are blocked with an error
- A backup of the original plan file is saved to `.humanize-loop.local/<timestamp>/plan-backup.md`

#### Case 4: Outside repo without `--commit-plan-file`
- **Setup**: No restrictions
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing with recovery options

**Key principle**: Plan file content changes are detected **before prompt processing** via the UserPromptSubmit hook. This blocks work from starting with a stale plan, giving the user a chance to restart the loop or restore the original plan.

## Prerequisites

Required tools:
**Required environment:**
- Git repository with at least one commit
- Plan file path must not contain spaces or special characters (`[ ] * ? { } | ( ) ^ $ \`)

**Required tools:**
- `codex` - OpenAI Codex CLI (for review)

Check if Codex is available:
```bash
codex --version
```

**Note on upgrading from pre-1.1.2:**
If you have an active RLCR loop started with a version before 1.1.2, the loop will be automatically terminated on the next stop attempt. Your work is preserved in `.humanize-loop.local/` - simply start a new loop with the updated plugin.

## Directory Structure

```
Expand DownExpand Up@@ -243,7 +285,8 @@ humanize/
When loop is active, creates: `.humanize-loop.local/<TIMESTAMP>/`

**Files Created**:
- `state.md` - Current round, config (YAML frontmatter)
- `state.md` - Current round, config (YAML frontmatter with `start_commit`, `commit_plan_file`, etc.)
- `plan-backup.md` - Backup copy of the original plan file at loop start
- `goal-tracker.md` - Immutable (goals/AC) + Mutable (active tasks, deferred, etc.)
- `round-N-prompt.md` - Instructions FROM Codex TO Claude
- `round-N-summary.md` - Work summary written BY Claude
Expand Down
8 changes: 6 additions & 2 deletions commands/cancel-rlcr-loop.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
description: "Cancel active RLCR loop"
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(rm .humanize-loop.local/*/state.md)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(for f in .humanize-loop.local/*/state.md*)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
hide-from-slash-command-tool: "true"
---

Expand All@@ -18,7 +18,11 @@ ls .humanize-loop.local/*/state.md 2>/dev/null || echo "NO_LOOP"

3. **If state file(s) found**:
- Read the state file to get the current round number
- Remove the state file(s) using: `rm .humanize-loop.local/*/state.md`
- Rename the state file(s) to cancelled-state.md:
```bash
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```
- Report: "Cancelled RLCR loop (was at round N of M)"

The loop directory with summaries and review results will be preserved for reference.
The cancelled-state.md file can be renamed back to state.md to manually restart the loop if needed.
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
{
"description": "Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-validator.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write",
Expand Down
169 changes: 169 additions & 0 deletions hooks/lib/loop-common.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,46 @@ if ! validate_template_dir "$TEMPLATE_DIR" 2>/dev/null; then
echo "Warning: Template directory validation failed. Using inline fallbacks." >&2
fi

# Stop an active loop by renaming state.md to <prefix>-state.md
# This preserves the state file for manual inspection/restart if needed
# The prefix indicates why the loop was stopped:
# - completed: Normal completion (Codex said COMPLETE)
# - stopped: Stagnation/circuit breaker (Codex said STOP, or max iterations)
# - unexpected: Error conditions (corruption, legacy state, branch change)
# - cancelled: User manually cancelled
# Usage: stop_loop "$STATE_FILE" "completed|stopped|unexpected|cancelled"
stop_loop() {
local state_file="$1"
local prefix="${2:-stopped}" # Default to "stopped" for backward compatibility
if [[ -f "$state_file" ]]; then
local new_file="${state_file%state.md}${prefix}-state.md"
mv "$state_file" "$new_file" 2>/dev/null || rm -f "$state_file"
fi
}

# Check if HEAD is a descendant of start_commit
# Returns 0 if HEAD is descendant (valid), 1 if not (user checked out older branch)
# Usage: check_start_commit_ancestry "$START_COMMIT"
check_start_commit_ancestry() {
local start_commit="$1"

# If no start_commit or not in a git repo, skip check
if [[ -z "$start_commit" ]]; then
return 0
fi

if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
return 0
fi

# Check if start_commit is an ancestor of HEAD
if git merge-base --is-ancestor "$start_commit" HEAD 2>/dev/null; then
return 0
else
return 1
fi
}

# Find the most recent active loop directory
# Only checks the newest directory - older directories are ignored even if they have state.md
# This prevents "zombie" loops from being revived after abnormal exits
Expand DownExpand Up@@ -62,6 +102,135 @@ to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# Normalize a path: remove double slashes, resolve . and .. components
# Usage: _normalize_path "/home//user/../project/./src"
# Output: "/home/project/src"
# Note: This is a helper for get_relative_path, defined at top level for zsh compatibility
_normalize_path() {
# Enable shwordsplit for zsh (localoptions makes it local to this function)
[[ -n "$ZSH_VERSION" ]] && setopt localoptions shwordsplit

local path="$1"
# Remove trailing slash
path="${path%/}"
# Remove double slashes
while [[ "$path" == *//* ]]; do
path="${path//\/\//\/}"
done
# Resolve . and .. components by iterating over path parts
local result="" part old_ifs="$IFS"
IFS='/'
set -f # Disable glob expansion
for part in ${path#/}; do
if [[ -z "$part" ]] || [[ "$part" == "." ]]; then
continue
elif [[ "$part" == ".." ]]; then
# Remove last component from result
result="${result%/*}"
else
result="$result/$part"
fi
done
set +f # Re-enable glob expansion
IFS="$old_ifs"
if [[ -z "$result" ]]; then
echo "/"
else
echo "$result"
fi
}

# Compute relative path from base to target (portable across Linux/macOS/BSD)
# Usage: get_relative_path "/base/dir" "/base/dir/sub/file.txt"
# Output: "sub/file.txt"
get_relative_path() {
# Enable 0-indexed arrays in zsh for bash compatibility
[[ -n "$ZSH_VERSION" ]] && setopt localoptions ksharrays

local base="$1"
local target="$2"

# Normalize paths (resolve symlinks, remove trailing slashes)
local base_real target_real
base_real=$(cd "$base" 2>/dev/null && pwd -P) || base_real="$base"
target_real=$(realpath "$target" 2>/dev/null) || target_real="$target"

# Try GNU realpath --relative-to (Linux)
if realpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi

# Try grealpath (macOS with coreutils)
if command -v grealpath &>/dev/null; then
if grealpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi
fi

# Fallback: compute relative path in bash/zsh (works for inside/outside base)
if [[ "$base_real" == /* ]] && [[ "$target_real" == /* ]]; then
local base_clean target_clean

# Normalize paths using top-level helper (for zsh compatibility)
base_clean=$(_normalize_path "$base_real")
target_clean=$(_normalize_path "$target_real")
[[ -z "$base_clean" ]] && base_clean="/"
[[ -z "$target_clean" ]] && target_clean="/"

if [[ "$base_clean" == "$target_clean" ]]; then
echo "."
return 0
fi

local base_trim target_trim
base_trim="${base_clean#/}"
target_trim="${target_clean#/}"

# Split paths into arrays (bash/zsh compatible using awk)
# This avoids read -a which is bash-specific
local base_parts target_parts rel_parts
base_parts=$(echo "$base_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')
target_parts=$(echo "$target_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')

# Convert to arrays using while read (portable)
local -a base_arr target_arr rel_arr
while IFS= read -r part; do
[[ -n "$part" ]] && base_arr+=("$part")
done <<< "$base_parts"
while IFS= read -r part; do
[[ -n "$part" ]] && target_arr+=("$part")
done <<< "$target_parts"

# Find common prefix length
local i=0
while [[ $i -lt ${#base_arr[@]} ]] && [[ $i -lt ${#target_arr[@]} ]] && \
[[ "${base_arr[$i]}" == "${target_arr[$i]}" ]]; do
((i++))
done

# Add ".." for each remaining base component
local j
for ((j=i; j<${#base_arr[@]}; j++)); do
rel_arr+=("..")
done
# Add remaining target components
for ((j=i; j<${#target_arr[@]}; j++)); do
rel_arr+=("${target_arr[$j]}")
done

if (( ${#rel_arr[@]} == 0 )); then
echo "."
else
local IFS='/'
echo "${rel_arr[*]}"
fi
return 0
fi

# Cannot compute relative path, return basename as last resort
basename "$target"
Comment on lines +229 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect external plan files correctly in get_relative_path

On macOS (or any environment without GNU realpath --relative-to and without grealpath), get_relative_path falls back to basename when the target isn’t under the base directory. Downstream logic in setup-rlcr-loop.sh and loop-plan-validator.sh treats “outside repo” as PLAN_FILE_REL starting with ../, so returning just the basename misclassifies an external plan file as inside the repo. That means the --commit-plan-file outside-repo conflict won’t trigger, and it can even incorrectly “match” a tracked file at the repo root with the same basename. This breaks the intended Case 3 behavior on macOS. Consider returning a true relative path (e.g., via python -c 'os.path.relpath') or an absolute path and updating the inside/outside check accordingly.

Useful? React with 👍 / 👎.

}

# Check if a path (lowercase) matches a round file pattern
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos"
is_round_file_type() {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9cdc84d
Add --commit-plan-file option for plan file version control
SihaoLiu Jan 14, 2026
d977d3b
Add validation for plan file outside project with --commit-plan-file
SihaoLiu Jan 14, 2026
a05f080
Fix PROJECT_ROOT unbound variable and improve plan file modification …
SihaoLiu Jan 15, 2026
8fc5d01
Update version bump rules for feature branches vs main branch
SihaoLiu Jan 15, 2026
425b630
Fix plan file handling for all four cases (P1, P2, P3)
SihaoLiu Jan 15, 2026
a86bc04
Fix stop hook to follow Claude Code hooks spec
SihaoLiu Jan 15, 2026
9bc8214
Simplify plan file handling code
SihaoLiu Jan 15, 2026
9555c5c
Fix plan file handling edge cases (P1-P4)
SihaoLiu Jan 15, 2026
f112744
Move plan file validation to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
6d02655
Move pre-1.1.2 state file check to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
42009ed
Fix pre-1.1.2 loop handling and portable path resolution
SihaoLiu Jan 15, 2026
34da7fa
Fix Python fallback injection, test signing, and README Case 4
SihaoLiu Jan 15, 2026
bc0fb24
Improve get_relative_path to handle paths outside base directory
SihaoLiu Jan 15, 2026
6a18c15
Add pre-commit check to block git commit when plan file is staged
SihaoLiu Jan 15, 2026
5dc3a6a
Fix --commit-plan-file validation for monorepo subdirectories
SihaoLiu Jan 15, 2026
54147e1
Fix plan validator inside/outside repo check for monorepo subdirectories
SihaoLiu Jan 15, 2026
1706bbd
Fix git status/diff path matching for monorepo subdirectories
SihaoLiu Jan 15, 2026
eb32366
Fix regex escaping for plan file paths with + character
SihaoLiu Jan 15, 2026
00930e9
Add git ancestry check and prefixed state file termination
SihaoLiu Jan 15, 2026
9c075fe
Fix git commands to use git-relative path in plan validator
SihaoLiu Jan 15, 2026
4dd1ca2
Fix code review issues for monorepo and POSIX compatibility
SihaoLiu Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@
"name": "humanize",
"source": "./",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1"
"version": "1.1.2"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"name": "humanize",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1",
"version": "1.1.2",
"author": {
"name": "humania-org"
},
Expand Down
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ This is a Claude Code plugin that provides iterative development with Codex revi

# Humanize Project Rules
- Everything about this project, including but not limited to implementations, comments, tests and documentations should be in English. No Emoji or CJK char is allowed.
- **MANDATORY**: Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). This applies to ALL commits without exception - bug fixes, features, documentation changes, etc. Increment the patch version (e.g., 1.0.0 -> 1.0.1) for each commit.
- **Version Update Rules**:
- **On feature branches** (not `main` or `master`): No need to bump version on every commit. However, ensure the branch has an incremental version update compared to the default main branch before merging.
- **On default branch** (`main` or `master`): Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). Increment the patch version (e.g., 1.0.0 -> 1.0.1, 1.0.9 -> 1.1.0, 3.4.7 -> 4.0.0) for each commit.
- Every `git push` or `git commit` MUST confirm with user first, MUST NOT commit or push to remote without check with user.
- Version number must be in format of `X.Y.Z` where X/Y/Z is numeric number. Version MUST NOT include anything other than `X.Y.Z`. For example, a good version is `9.732.42`; Bad version examples (MUST NOT USE): `3.22.7-alpha` (extra "-alpha" string), `9.77.2 (2026-01-07)` (useless date/timestamp).
10 changes: 10 additions & 0 deletions .github/workflows/template-test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,13 +6,15 @@ on:
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'
pull_request:
branches: ['**']
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'

Expand DownExpand Up@@ -47,6 +49,14 @@ jobs:
chmod +x tests/test-template-references.sh
./tests/test-template-references.sh

- name: Run plan file handling tests
run: |
echo "========================================"
echo "Running plan file handling tests"
echo "========================================"
chmod +x tests/test-plan-file-handling.sh
./tests/test-plan-file-handling.sh

- name: Verify all hook scripts still work
run: |
echo "========================================"
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.1**
**Current Version: 1.1.2**

> Derived from the [GAAC (GitHub-as-a-Context)](https://github.com/SihaoLiu/gaac) project.

Expand DownExpand Up@@ -115,18 +115,26 @@ This provides a real-time dashboard showing:

- **Loop state**: Controlled solely by the presence of `.humanize-loop.local/*/state.md`
- **Resume**: Simply restart Claude Code in the same directory - the loop continues automatically
- **Cancel**: Remove the state file to stop the loop permanently
- **Cancel**: Rename the state file with a prefix to stop the loop

```bash
# Cancel the active loop
/humanize:cancel-rlcr-loop

# Or manually remove state file
rm .humanize-loop.local/*/state.md
# Or manually rename state file
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```

The loop directory with all summaries and review results is preserved for reference.

**State file prefixes indicate termination reason:**
- `completed-state.md` - Normal completion (Codex said COMPLETE)
- `stopped-state.md` - Stagnation/circuit breaker (Codex said STOP, or max iterations)
- `cancelled-state.md` - User manually cancelled
- `unexpected-state.md` - Error conditions (corruption, legacy state, branch change)

Any prefixed state file can be renamed back to `state.md` to manually restart the loop.

## Goal Tracker System

Humanize uses a **Goal Tracker** to prevent goal drift across iterations:
Expand DownExpand Up@@ -196,19 +204,53 @@ OPTIONS:
--codex-timeout <SECONDS>
Timeout for each Codex review in seconds (default: 5400)
--push-every-round Require git push after each round (default: commits stay local)
--commit-plan-file Include the plan file in commits (default: plan file stays uncommitted)
-h, --help Show help message
```

### Plan File Handling

The plan file behavior depends on the `--commit-plan-file` flag and whether the plan file is inside or outside the git repository:

#### Case 1: Inside repo with `--commit-plan-file`
- **Setup**: Plan file must be tracked (committed) AND clean (no uncommitted changes)
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing
- **Stop hook**: If plan file has uncommitted changes (dirty), allows stop with error message
- Plan file changes must be committed like any other file

#### Case 2: Outside repo with `--commit-plan-file`
- **Setup**: Early failure - this configuration is not allowed
- **Stop hook**: If detected (e.g., from manual state file edit), allows stop with conflict error

#### Case 3: Inside repo without `--commit-plan-file` (default)
- **Setup**: Plan file can be tracked or untracked, dirty or clean
- **UserPromptSubmit hook**: If plan file content differs from backup (or is missing), **blocks prompt** before processing with recovery options
- Accidental commits of the plan file are blocked with an error
- A backup of the original plan file is saved to `.humanize-loop.local/<timestamp>/plan-backup.md`

#### Case 4: Outside repo without `--commit-plan-file`
- **Setup**: No restrictions
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing with recovery options

**Key principle**: Plan file content changes are detected **before prompt processing** via the UserPromptSubmit hook. This blocks work from starting with a stale plan, giving the user a chance to restart the loop or restore the original plan.

## Prerequisites

Required tools:
**Required environment:**
- Git repository with at least one commit
- Plan file path must not contain spaces or special characters (`[ ] * ? { } | ( ) ^ $ \`)

**Required tools:**
- `codex` - OpenAI Codex CLI (for review)

Check if Codex is available:
```bash
codex --version
```

**Note on upgrading from pre-1.1.2:**
If you have an active RLCR loop started with a version before 1.1.2, the loop will be automatically terminated on the next stop attempt. Your work is preserved in `.humanize-loop.local/` - simply start a new loop with the updated plugin.

## Directory Structure

```
Expand DownExpand Up@@ -243,7 +285,8 @@ humanize/
When loop is active, creates: `.humanize-loop.local/<TIMESTAMP>/`

**Files Created**:
- `state.md` - Current round, config (YAML frontmatter)
- `state.md` - Current round, config (YAML frontmatter with `start_commit`, `commit_plan_file`, etc.)
- `plan-backup.md` - Backup copy of the original plan file at loop start
- `goal-tracker.md` - Immutable (goals/AC) + Mutable (active tasks, deferred, etc.)
- `round-N-prompt.md` - Instructions FROM Codex TO Claude
- `round-N-summary.md` - Work summary written BY Claude
Expand Down
8 changes: 6 additions & 2 deletions commands/cancel-rlcr-loop.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
description: "Cancel active RLCR loop"
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(rm .humanize-loop.local/*/state.md)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(for f in .humanize-loop.local/*/state.md*)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
hide-from-slash-command-tool: "true"
---

Expand All@@ -18,7 +18,11 @@ ls .humanize-loop.local/*/state.md 2>/dev/null || echo "NO_LOOP"

3. **If state file(s) found**:
- Read the state file to get the current round number
- Remove the state file(s) using: `rm .humanize-loop.local/*/state.md`
- Rename the state file(s) to cancelled-state.md:
```bash
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```
- Report: "Cancelled RLCR loop (was at round N of M)"

The loop directory with summaries and review results will be preserved for reference.
The cancelled-state.md file can be renamed back to state.md to manually restart the loop if needed.
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
{
"description": "Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-validator.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write",
Expand Down
169 changes: 169 additions & 0 deletions hooks/lib/loop-common.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,46 @@ if ! validate_template_dir "$TEMPLATE_DIR" 2>/dev/null; then
echo "Warning: Template directory validation failed. Using inline fallbacks." >&2
fi

# Stop an active loop by renaming state.md to <prefix>-state.md
# This preserves the state file for manual inspection/restart if needed
# The prefix indicates why the loop was stopped:
# - completed: Normal completion (Codex said COMPLETE)
# - stopped: Stagnation/circuit breaker (Codex said STOP, or max iterations)
# - unexpected: Error conditions (corruption, legacy state, branch change)
# - cancelled: User manually cancelled
# Usage: stop_loop "$STATE_FILE" "completed|stopped|unexpected|cancelled"
stop_loop() {
local state_file="$1"
local prefix="${2:-stopped}" # Default to "stopped" for backward compatibility
if [[ -f "$state_file" ]]; then
local new_file="${state_file%state.md}${prefix}-state.md"
mv "$state_file" "$new_file" 2>/dev/null || rm -f "$state_file"
fi
}

# Check if HEAD is a descendant of start_commit
# Returns 0 if HEAD is descendant (valid), 1 if not (user checked out older branch)
# Usage: check_start_commit_ancestry "$START_COMMIT"
check_start_commit_ancestry() {
local start_commit="$1"

# If no start_commit or not in a git repo, skip check
if [[ -z "$start_commit" ]]; then
return 0
fi

if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
return 0
fi

# Check if start_commit is an ancestor of HEAD
if git merge-base --is-ancestor "$start_commit" HEAD 2>/dev/null; then
return 0
else
return 1
fi
}

# Find the most recent active loop directory
# Only checks the newest directory - older directories are ignored even if they have state.md
# This prevents "zombie" loops from being revived after abnormal exits
Expand DownExpand Up@@ -62,6 +102,135 @@ to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# Normalize a path: remove double slashes, resolve . and .. components
# Usage: _normalize_path "/home//user/../project/./src"
# Output: "/home/project/src"
# Note: This is a helper for get_relative_path, defined at top level for zsh compatibility
_normalize_path() {
# Enable shwordsplit for zsh (localoptions makes it local to this function)
[[ -n "$ZSH_VERSION" ]] && setopt localoptions shwordsplit

local path="$1"
# Remove trailing slash
path="${path%/}"
# Remove double slashes
while [[ "$path" == *//* ]]; do
path="${path//\/\//\/}"
done
# Resolve . and .. components by iterating over path parts
local result="" part old_ifs="$IFS"
IFS='/'
set -f # Disable glob expansion
for part in ${path#/}; do
if [[ -z "$part" ]] || [[ "$part" == "." ]]; then
continue
elif [[ "$part" == ".." ]]; then
# Remove last component from result
result="${result%/*}"
else
result="$result/$part"
fi
done
set +f # Re-enable glob expansion
IFS="$old_ifs"
if [[ -z "$result" ]]; then
echo "/"
else
echo "$result"
fi
}

# Compute relative path from base to target (portable across Linux/macOS/BSD)
# Usage: get_relative_path "/base/dir" "/base/dir/sub/file.txt"
# Output: "sub/file.txt"
get_relative_path() {
# Enable 0-indexed arrays in zsh for bash compatibility
[[ -n "$ZSH_VERSION" ]] && setopt localoptions ksharrays

local base="$1"
local target="$2"

# Normalize paths (resolve symlinks, remove trailing slashes)
local base_real target_real
base_real=$(cd "$base" 2>/dev/null && pwd -P) || base_real="$base"
target_real=$(realpath "$target" 2>/dev/null) || target_real="$target"

# Try GNU realpath --relative-to (Linux)
if realpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi

# Try grealpath (macOS with coreutils)
if command -v grealpath &>/dev/null; then
if grealpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi
fi

# Fallback: compute relative path in bash/zsh (works for inside/outside base)
if [[ "$base_real" == /* ]] && [[ "$target_real" == /* ]]; then
local base_clean target_clean

# Normalize paths using top-level helper (for zsh compatibility)
base_clean=$(_normalize_path "$base_real")
target_clean=$(_normalize_path "$target_real")
[[ -z "$base_clean" ]] && base_clean="/"
[[ -z "$target_clean" ]] && target_clean="/"

if [[ "$base_clean" == "$target_clean" ]]; then
echo "."
return 0
fi

local base_trim target_trim
base_trim="${base_clean#/}"
target_trim="${target_clean#/}"

# Split paths into arrays (bash/zsh compatible using awk)
# This avoids read -a which is bash-specific
local base_parts target_parts rel_parts
base_parts=$(echo "$base_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')
target_parts=$(echo "$target_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')

# Convert to arrays using while read (portable)
local -a base_arr target_arr rel_arr
while IFS= read -r part; do
[[ -n "$part" ]] && base_arr+=("$part")
done <<< "$base_parts"
while IFS= read -r part; do
[[ -n "$part" ]] && target_arr+=("$part")
done <<< "$target_parts"

# Find common prefix length
local i=0
while [[ $i -lt ${#base_arr[@]} ]] && [[ $i -lt ${#target_arr[@]} ]] && \
[[ "${base_arr[$i]}" == "${target_arr[$i]}" ]]; do
((i++))
done

# Add ".." for each remaining base component
local j
for ((j=i; j<${#base_arr[@]}; j++)); do
rel_arr+=("..")
done
# Add remaining target components
for ((j=i; j<${#target_arr[@]}; j++)); do
rel_arr+=("${target_arr[$j]}")
done

if (( ${#rel_arr[@]} == 0 )); then
echo "."
else
local IFS='/'
echo "${rel_arr[*]}"
fi
return 0
fi

# Cannot compute relative path, return basename as last resort
basename "$target"
Comment on lines +229 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect external plan files correctly in get_relative_path

On macOS (or any environment without GNU realpath --relative-to and without grealpath), get_relative_path falls back to basename when the target isn’t under the base directory. Downstream logic in setup-rlcr-loop.sh and loop-plan-validator.sh treats “outside repo” as PLAN_FILE_REL starting with ../, so returning just the basename misclassifies an external plan file as inside the repo. That means the --commit-plan-file outside-repo conflict won’t trigger, and it can even incorrectly “match” a tracked file at the repo root with the same basename. This breaks the intended Case 3 behavior on macOS. Consider returning a true relative path (e.g., via python -c 'os.path.relpath') or an absolute path and updating the inside/outside check accordingly.

Useful? React with 👍 / 👎.

}

# Check if a path (lowercase) matches a round file pattern
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos"
is_round_file_type() {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9cdc84d
Add --commit-plan-file option for plan file version control
SihaoLiu Jan 14, 2026
d977d3b
Add validation for plan file outside project with --commit-plan-file
SihaoLiu Jan 14, 2026
a05f080
Fix PROJECT_ROOT unbound variable and improve plan file modification …
SihaoLiu Jan 15, 2026
8fc5d01
Update version bump rules for feature branches vs main branch
SihaoLiu Jan 15, 2026
425b630
Fix plan file handling for all four cases (P1, P2, P3)
SihaoLiu Jan 15, 2026
a86bc04
Fix stop hook to follow Claude Code hooks spec
SihaoLiu Jan 15, 2026
9bc8214
Simplify plan file handling code
SihaoLiu Jan 15, 2026
9555c5c
Fix plan file handling edge cases (P1-P4)
SihaoLiu Jan 15, 2026
f112744
Move plan file validation to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
6d02655
Move pre-1.1.2 state file check to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
42009ed
Fix pre-1.1.2 loop handling and portable path resolution
SihaoLiu Jan 15, 2026
34da7fa
Fix Python fallback injection, test signing, and README Case 4
SihaoLiu Jan 15, 2026
bc0fb24
Improve get_relative_path to handle paths outside base directory
SihaoLiu Jan 15, 2026
6a18c15
Add pre-commit check to block git commit when plan file is staged
SihaoLiu Jan 15, 2026
5dc3a6a
Fix --commit-plan-file validation for monorepo subdirectories
SihaoLiu Jan 15, 2026
54147e1
Fix plan validator inside/outside repo check for monorepo subdirectories
SihaoLiu Jan 15, 2026
1706bbd
Fix git status/diff path matching for monorepo subdirectories
SihaoLiu Jan 15, 2026
eb32366
Fix regex escaping for plan file paths with + character
SihaoLiu Jan 15, 2026
00930e9
Add git ancestry check and prefixed state file termination
SihaoLiu Jan 15, 2026
9c075fe
Fix git commands to use git-relative path in plan validator
SihaoLiu Jan 15, 2026
4dd1ca2
Fix code review issues for monorepo and POSIX compatibility
SihaoLiu Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@
"name": "humanize",
"source": "./",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1"
"version": "1.1.2"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"name": "humanize",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1",
"version": "1.1.2",
"author": {
"name": "humania-org"
},
Expand Down
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ This is a Claude Code plugin that provides iterative development with Codex revi

# Humanize Project Rules
- Everything about this project, including but not limited to implementations, comments, tests and documentations should be in English. No Emoji or CJK char is allowed.
- **MANDATORY**: Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). This applies to ALL commits without exception - bug fixes, features, documentation changes, etc. Increment the patch version (e.g., 1.0.0 -> 1.0.1) for each commit.
- **Version Update Rules**:
- **On feature branches** (not `main` or `master`): No need to bump version on every commit. However, ensure the branch has an incremental version update compared to the default main branch before merging.
- **On default branch** (`main` or `master`): Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). Increment the patch version (e.g., 1.0.0 -> 1.0.1, 1.0.9 -> 1.1.0, 3.4.7 -> 4.0.0) for each commit.
- Every `git push` or `git commit` MUST confirm with user first, MUST NOT commit or push to remote without check with user.
- Version number must be in format of `X.Y.Z` where X/Y/Z is numeric number. Version MUST NOT include anything other than `X.Y.Z`. For example, a good version is `9.732.42`; Bad version examples (MUST NOT USE): `3.22.7-alpha` (extra "-alpha" string), `9.77.2 (2026-01-07)` (useless date/timestamp).
10 changes: 10 additions & 0 deletions .github/workflows/template-test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,13 +6,15 @@ on:
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'
pull_request:
branches: ['**']
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'

Expand DownExpand Up@@ -47,6 +49,14 @@ jobs:
chmod +x tests/test-template-references.sh
./tests/test-template-references.sh

- name: Run plan file handling tests
run: |
echo "========================================"
echo "Running plan file handling tests"
echo "========================================"
chmod +x tests/test-plan-file-handling.sh
./tests/test-plan-file-handling.sh

- name: Verify all hook scripts still work
run: |
echo "========================================"
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.1**
**Current Version: 1.1.2**

> Derived from the [GAAC (GitHub-as-a-Context)](https://github.com/SihaoLiu/gaac) project.

Expand DownExpand Up@@ -115,18 +115,26 @@ This provides a real-time dashboard showing:

- **Loop state**: Controlled solely by the presence of `.humanize-loop.local/*/state.md`
- **Resume**: Simply restart Claude Code in the same directory - the loop continues automatically
- **Cancel**: Remove the state file to stop the loop permanently
- **Cancel**: Rename the state file with a prefix to stop the loop

```bash
# Cancel the active loop
/humanize:cancel-rlcr-loop

# Or manually remove state file
rm .humanize-loop.local/*/state.md
# Or manually rename state file
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```

The loop directory with all summaries and review results is preserved for reference.

**State file prefixes indicate termination reason:**
- `completed-state.md` - Normal completion (Codex said COMPLETE)
- `stopped-state.md` - Stagnation/circuit breaker (Codex said STOP, or max iterations)
- `cancelled-state.md` - User manually cancelled
- `unexpected-state.md` - Error conditions (corruption, legacy state, branch change)

Any prefixed state file can be renamed back to `state.md` to manually restart the loop.

## Goal Tracker System

Humanize uses a **Goal Tracker** to prevent goal drift across iterations:
Expand DownExpand Up@@ -196,19 +204,53 @@ OPTIONS:
--codex-timeout <SECONDS>
Timeout for each Codex review in seconds (default: 5400)
--push-every-round Require git push after each round (default: commits stay local)
--commit-plan-file Include the plan file in commits (default: plan file stays uncommitted)
-h, --help Show help message
```

### Plan File Handling

The plan file behavior depends on the `--commit-plan-file` flag and whether the plan file is inside or outside the git repository:

#### Case 1: Inside repo with `--commit-plan-file`
- **Setup**: Plan file must be tracked (committed) AND clean (no uncommitted changes)
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing
- **Stop hook**: If plan file has uncommitted changes (dirty), allows stop with error message
- Plan file changes must be committed like any other file

#### Case 2: Outside repo with `--commit-plan-file`
- **Setup**: Early failure - this configuration is not allowed
- **Stop hook**: If detected (e.g., from manual state file edit), allows stop with conflict error

#### Case 3: Inside repo without `--commit-plan-file` (default)
- **Setup**: Plan file can be tracked or untracked, dirty or clean
- **UserPromptSubmit hook**: If plan file content differs from backup (or is missing), **blocks prompt** before processing with recovery options
- Accidental commits of the plan file are blocked with an error
- A backup of the original plan file is saved to `.humanize-loop.local/<timestamp>/plan-backup.md`

#### Case 4: Outside repo without `--commit-plan-file`
- **Setup**: No restrictions
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing with recovery options

**Key principle**: Plan file content changes are detected **before prompt processing** via the UserPromptSubmit hook. This blocks work from starting with a stale plan, giving the user a chance to restart the loop or restore the original plan.

## Prerequisites

Required tools:
**Required environment:**
- Git repository with at least one commit
- Plan file path must not contain spaces or special characters (`[ ] * ? { } | ( ) ^ $ \`)

**Required tools:**
- `codex` - OpenAI Codex CLI (for review)

Check if Codex is available:
```bash
codex --version
```

**Note on upgrading from pre-1.1.2:**
If you have an active RLCR loop started with a version before 1.1.2, the loop will be automatically terminated on the next stop attempt. Your work is preserved in `.humanize-loop.local/` - simply start a new loop with the updated plugin.

## Directory Structure

```
Expand DownExpand Up@@ -243,7 +285,8 @@ humanize/
When loop is active, creates: `.humanize-loop.local/<TIMESTAMP>/`

**Files Created**:
- `state.md` - Current round, config (YAML frontmatter)
- `state.md` - Current round, config (YAML frontmatter with `start_commit`, `commit_plan_file`, etc.)
- `plan-backup.md` - Backup copy of the original plan file at loop start
- `goal-tracker.md` - Immutable (goals/AC) + Mutable (active tasks, deferred, etc.)
- `round-N-prompt.md` - Instructions FROM Codex TO Claude
- `round-N-summary.md` - Work summary written BY Claude
Expand Down
8 changes: 6 additions & 2 deletions commands/cancel-rlcr-loop.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
description: "Cancel active RLCR loop"
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(rm .humanize-loop.local/*/state.md)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(for f in .humanize-loop.local/*/state.md*)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
hide-from-slash-command-tool: "true"
---

Expand All@@ -18,7 +18,11 @@ ls .humanize-loop.local/*/state.md 2>/dev/null || echo "NO_LOOP"

3. **If state file(s) found**:
- Read the state file to get the current round number
- Remove the state file(s) using: `rm .humanize-loop.local/*/state.md`
- Rename the state file(s) to cancelled-state.md:
```bash
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```
- Report: "Cancelled RLCR loop (was at round N of M)"

The loop directory with summaries and review results will be preserved for reference.
The cancelled-state.md file can be renamed back to state.md to manually restart the loop if needed.
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
{
"description": "Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-validator.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write",
Expand Down
169 changes: 169 additions & 0 deletions hooks/lib/loop-common.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,46 @@ if ! validate_template_dir "$TEMPLATE_DIR" 2>/dev/null; then
echo "Warning: Template directory validation failed. Using inline fallbacks." >&2
fi

# Stop an active loop by renaming state.md to <prefix>-state.md
# This preserves the state file for manual inspection/restart if needed
# The prefix indicates why the loop was stopped:
# - completed: Normal completion (Codex said COMPLETE)
# - stopped: Stagnation/circuit breaker (Codex said STOP, or max iterations)
# - unexpected: Error conditions (corruption, legacy state, branch change)
# - cancelled: User manually cancelled
# Usage: stop_loop "$STATE_FILE" "completed|stopped|unexpected|cancelled"
stop_loop() {
local state_file="$1"
local prefix="${2:-stopped}" # Default to "stopped" for backward compatibility
if [[ -f "$state_file" ]]; then
local new_file="${state_file%state.md}${prefix}-state.md"
mv "$state_file" "$new_file" 2>/dev/null || rm -f "$state_file"
fi
}

# Check if HEAD is a descendant of start_commit
# Returns 0 if HEAD is descendant (valid), 1 if not (user checked out older branch)
# Usage: check_start_commit_ancestry "$START_COMMIT"
check_start_commit_ancestry() {
local start_commit="$1"

# If no start_commit or not in a git repo, skip check
if [[ -z "$start_commit" ]]; then
return 0
fi

if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
return 0
fi

# Check if start_commit is an ancestor of HEAD
if git merge-base --is-ancestor "$start_commit" HEAD 2>/dev/null; then
return 0
else
return 1
fi
}

# Find the most recent active loop directory
# Only checks the newest directory - older directories are ignored even if they have state.md
# This prevents "zombie" loops from being revived after abnormal exits
Expand DownExpand Up@@ -62,6 +102,135 @@ to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# Normalize a path: remove double slashes, resolve . and .. components
# Usage: _normalize_path "/home//user/../project/./src"
# Output: "/home/project/src"
# Note: This is a helper for get_relative_path, defined at top level for zsh compatibility
_normalize_path() {
# Enable shwordsplit for zsh (localoptions makes it local to this function)
[[ -n "$ZSH_VERSION" ]] && setopt localoptions shwordsplit

local path="$1"
# Remove trailing slash
path="${path%/}"
# Remove double slashes
while [[ "$path" == *//* ]]; do
path="${path//\/\//\/}"
done
# Resolve . and .. components by iterating over path parts
local result="" part old_ifs="$IFS"
IFS='/'
set -f # Disable glob expansion
for part in ${path#/}; do
if [[ -z "$part" ]] || [[ "$part" == "." ]]; then
continue
elif [[ "$part" == ".." ]]; then
# Remove last component from result
result="${result%/*}"
else
result="$result/$part"
fi
done
set +f # Re-enable glob expansion
IFS="$old_ifs"
if [[ -z "$result" ]]; then
echo "/"
else
echo "$result"
fi
}

# Compute relative path from base to target (portable across Linux/macOS/BSD)
# Usage: get_relative_path "/base/dir" "/base/dir/sub/file.txt"
# Output: "sub/file.txt"
get_relative_path() {
# Enable 0-indexed arrays in zsh for bash compatibility
[[ -n "$ZSH_VERSION" ]] && setopt localoptions ksharrays

local base="$1"
local target="$2"

# Normalize paths (resolve symlinks, remove trailing slashes)
local base_real target_real
base_real=$(cd "$base" 2>/dev/null && pwd -P) || base_real="$base"
target_real=$(realpath "$target" 2>/dev/null) || target_real="$target"

# Try GNU realpath --relative-to (Linux)
if realpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi

# Try grealpath (macOS with coreutils)
if command -v grealpath &>/dev/null; then
if grealpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi
fi

# Fallback: compute relative path in bash/zsh (works for inside/outside base)
if [[ "$base_real" == /* ]] && [[ "$target_real" == /* ]]; then
local base_clean target_clean

# Normalize paths using top-level helper (for zsh compatibility)
base_clean=$(_normalize_path "$base_real")
target_clean=$(_normalize_path "$target_real")
[[ -z "$base_clean" ]] && base_clean="/"
[[ -z "$target_clean" ]] && target_clean="/"

if [[ "$base_clean" == "$target_clean" ]]; then
echo "."
return 0
fi

local base_trim target_trim
base_trim="${base_clean#/}"
target_trim="${target_clean#/}"

# Split paths into arrays (bash/zsh compatible using awk)
# This avoids read -a which is bash-specific
local base_parts target_parts rel_parts
base_parts=$(echo "$base_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')
target_parts=$(echo "$target_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')

# Convert to arrays using while read (portable)
local -a base_arr target_arr rel_arr
while IFS= read -r part; do
[[ -n "$part" ]] && base_arr+=("$part")
done <<< "$base_parts"
while IFS= read -r part; do
[[ -n "$part" ]] && target_arr+=("$part")
done <<< "$target_parts"

# Find common prefix length
local i=0
while [[ $i -lt ${#base_arr[@]} ]] && [[ $i -lt ${#target_arr[@]} ]] && \
[[ "${base_arr[$i]}" == "${target_arr[$i]}" ]]; do
((i++))
done

# Add ".." for each remaining base component
local j
for ((j=i; j<${#base_arr[@]}; j++)); do
rel_arr+=("..")
done
# Add remaining target components
for ((j=i; j<${#target_arr[@]}; j++)); do
rel_arr+=("${target_arr[$j]}")
done

if (( ${#rel_arr[@]} == 0 )); then
echo "."
else
local IFS='/'
echo "${rel_arr[*]}"
fi
return 0
fi

# Cannot compute relative path, return basename as last resort
basename "$target"
Comment on lines +229 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect external plan files correctly in get_relative_path

On macOS (or any environment without GNU realpath --relative-to and without grealpath), get_relative_path falls back to basename when the target isn’t under the base directory. Downstream logic in setup-rlcr-loop.sh and loop-plan-validator.sh treats “outside repo” as PLAN_FILE_REL starting with ../, so returning just the basename misclassifies an external plan file as inside the repo. That means the --commit-plan-file outside-repo conflict won’t trigger, and it can even incorrectly “match” a tracked file at the repo root with the same basename. This breaks the intended Case 3 behavior on macOS. Consider returning a true relative path (e.g., via python -c 'os.path.relpath') or an absolute path and updating the inside/outside check accordingly.

Useful? React with 👍 / 👎.

}

# Check if a path (lowercase) matches a round file pattern
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos"
is_round_file_type() {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9cdc84d
Add --commit-plan-file option for plan file version control
SihaoLiu Jan 14, 2026
d977d3b
Add validation for plan file outside project with --commit-plan-file
SihaoLiu Jan 14, 2026
a05f080
Fix PROJECT_ROOT unbound variable and improve plan file modification …
SihaoLiu Jan 15, 2026
8fc5d01
Update version bump rules for feature branches vs main branch
SihaoLiu Jan 15, 2026
425b630
Fix plan file handling for all four cases (P1, P2, P3)
SihaoLiu Jan 15, 2026
a86bc04
Fix stop hook to follow Claude Code hooks spec
SihaoLiu Jan 15, 2026
9bc8214
Simplify plan file handling code
SihaoLiu Jan 15, 2026
9555c5c
Fix plan file handling edge cases (P1-P4)
SihaoLiu Jan 15, 2026
f112744
Move plan file validation to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
6d02655
Move pre-1.1.2 state file check to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
42009ed
Fix pre-1.1.2 loop handling and portable path resolution
SihaoLiu Jan 15, 2026
34da7fa
Fix Python fallback injection, test signing, and README Case 4
SihaoLiu Jan 15, 2026
bc0fb24
Improve get_relative_path to handle paths outside base directory
SihaoLiu Jan 15, 2026
6a18c15
Add pre-commit check to block git commit when plan file is staged
SihaoLiu Jan 15, 2026
5dc3a6a
Fix --commit-plan-file validation for monorepo subdirectories
SihaoLiu Jan 15, 2026
54147e1
Fix plan validator inside/outside repo check for monorepo subdirectories
SihaoLiu Jan 15, 2026
1706bbd
Fix git status/diff path matching for monorepo subdirectories
SihaoLiu Jan 15, 2026
eb32366
Fix regex escaping for plan file paths with + character
SihaoLiu Jan 15, 2026
00930e9
Add git ancestry check and prefixed state file termination
SihaoLiu Jan 15, 2026
9c075fe
Fix git commands to use git-relative path in plan validator
SihaoLiu Jan 15, 2026
4dd1ca2
Fix code review issues for monorepo and POSIX compatibility
SihaoLiu Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@
"name": "humanize",
"source": "./",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1"
"version": "1.1.2"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"name": "humanize",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1",
"version": "1.1.2",
"author": {
"name": "humania-org"
},
Expand Down
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ This is a Claude Code plugin that provides iterative development with Codex revi

# Humanize Project Rules
- Everything about this project, including but not limited to implementations, comments, tests and documentations should be in English. No Emoji or CJK char is allowed.
- **MANDATORY**: Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). This applies to ALL commits without exception - bug fixes, features, documentation changes, etc. Increment the patch version (e.g., 1.0.0 -> 1.0.1) for each commit.
- **Version Update Rules**:
- **On feature branches** (not `main` or `master`): No need to bump version on every commit. However, ensure the branch has an incremental version update compared to the default main branch before merging.
- **On default branch** (`main` or `master`): Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). Increment the patch version (e.g., 1.0.0 -> 1.0.1, 1.0.9 -> 1.1.0, 3.4.7 -> 4.0.0) for each commit.
- Every `git push` or `git commit` MUST confirm with user first, MUST NOT commit or push to remote without check with user.
- Version number must be in format of `X.Y.Z` where X/Y/Z is numeric number. Version MUST NOT include anything other than `X.Y.Z`. For example, a good version is `9.732.42`; Bad version examples (MUST NOT USE): `3.22.7-alpha` (extra "-alpha" string), `9.77.2 (2026-01-07)` (useless date/timestamp).
10 changes: 10 additions & 0 deletions .github/workflows/template-test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,13 +6,15 @@ on:
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'
pull_request:
branches: ['**']
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'

Expand DownExpand Up@@ -47,6 +49,14 @@ jobs:
chmod +x tests/test-template-references.sh
./tests/test-template-references.sh

- name: Run plan file handling tests
run: |
echo "========================================"
echo "Running plan file handling tests"
echo "========================================"
chmod +x tests/test-plan-file-handling.sh
./tests/test-plan-file-handling.sh

- name: Verify all hook scripts still work
run: |
echo "========================================"
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.1**
**Current Version: 1.1.2**

> Derived from the [GAAC (GitHub-as-a-Context)](https://github.com/SihaoLiu/gaac) project.

Expand DownExpand Up@@ -115,18 +115,26 @@ This provides a real-time dashboard showing:

- **Loop state**: Controlled solely by the presence of `.humanize-loop.local/*/state.md`
- **Resume**: Simply restart Claude Code in the same directory - the loop continues automatically
- **Cancel**: Remove the state file to stop the loop permanently
- **Cancel**: Rename the state file with a prefix to stop the loop

```bash
# Cancel the active loop
/humanize:cancel-rlcr-loop

# Or manually remove state file
rm .humanize-loop.local/*/state.md
# Or manually rename state file
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```

The loop directory with all summaries and review results is preserved for reference.

**State file prefixes indicate termination reason:**
- `completed-state.md` - Normal completion (Codex said COMPLETE)
- `stopped-state.md` - Stagnation/circuit breaker (Codex said STOP, or max iterations)
- `cancelled-state.md` - User manually cancelled
- `unexpected-state.md` - Error conditions (corruption, legacy state, branch change)

Any prefixed state file can be renamed back to `state.md` to manually restart the loop.

## Goal Tracker System

Humanize uses a **Goal Tracker** to prevent goal drift across iterations:
Expand DownExpand Up@@ -196,19 +204,53 @@ OPTIONS:
--codex-timeout <SECONDS>
Timeout for each Codex review in seconds (default: 5400)
--push-every-round Require git push after each round (default: commits stay local)
--commit-plan-file Include the plan file in commits (default: plan file stays uncommitted)
-h, --help Show help message
```

### Plan File Handling

The plan file behavior depends on the `--commit-plan-file` flag and whether the plan file is inside or outside the git repository:

#### Case 1: Inside repo with `--commit-plan-file`
- **Setup**: Plan file must be tracked (committed) AND clean (no uncommitted changes)
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing
- **Stop hook**: If plan file has uncommitted changes (dirty), allows stop with error message
- Plan file changes must be committed like any other file

#### Case 2: Outside repo with `--commit-plan-file`
- **Setup**: Early failure - this configuration is not allowed
- **Stop hook**: If detected (e.g., from manual state file edit), allows stop with conflict error

#### Case 3: Inside repo without `--commit-plan-file` (default)
- **Setup**: Plan file can be tracked or untracked, dirty or clean
- **UserPromptSubmit hook**: If plan file content differs from backup (or is missing), **blocks prompt** before processing with recovery options
- Accidental commits of the plan file are blocked with an error
- A backup of the original plan file is saved to `.humanize-loop.local/<timestamp>/plan-backup.md`

#### Case 4: Outside repo without `--commit-plan-file`
- **Setup**: No restrictions
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing with recovery options

**Key principle**: Plan file content changes are detected **before prompt processing** via the UserPromptSubmit hook. This blocks work from starting with a stale plan, giving the user a chance to restart the loop or restore the original plan.

## Prerequisites

Required tools:
**Required environment:**
- Git repository with at least one commit
- Plan file path must not contain spaces or special characters (`[ ] * ? { } | ( ) ^ $ \`)

**Required tools:**
- `codex` - OpenAI Codex CLI (for review)

Check if Codex is available:
```bash
codex --version
```

**Note on upgrading from pre-1.1.2:**
If you have an active RLCR loop started with a version before 1.1.2, the loop will be automatically terminated on the next stop attempt. Your work is preserved in `.humanize-loop.local/` - simply start a new loop with the updated plugin.

## Directory Structure

```
Expand DownExpand Up@@ -243,7 +285,8 @@ humanize/
When loop is active, creates: `.humanize-loop.local/<TIMESTAMP>/`

**Files Created**:
- `state.md` - Current round, config (YAML frontmatter)
- `state.md` - Current round, config (YAML frontmatter with `start_commit`, `commit_plan_file`, etc.)
- `plan-backup.md` - Backup copy of the original plan file at loop start
- `goal-tracker.md` - Immutable (goals/AC) + Mutable (active tasks, deferred, etc.)
- `round-N-prompt.md` - Instructions FROM Codex TO Claude
- `round-N-summary.md` - Work summary written BY Claude
Expand Down
8 changes: 6 additions & 2 deletions commands/cancel-rlcr-loop.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
description: "Cancel active RLCR loop"
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(rm .humanize-loop.local/*/state.md)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(for f in .humanize-loop.local/*/state.md*)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
hide-from-slash-command-tool: "true"
---

Expand All@@ -18,7 +18,11 @@ ls .humanize-loop.local/*/state.md 2>/dev/null || echo "NO_LOOP"

3. **If state file(s) found**:
- Read the state file to get the current round number
- Remove the state file(s) using: `rm .humanize-loop.local/*/state.md`
- Rename the state file(s) to cancelled-state.md:
```bash
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```
- Report: "Cancelled RLCR loop (was at round N of M)"

The loop directory with summaries and review results will be preserved for reference.
The cancelled-state.md file can be renamed back to state.md to manually restart the loop if needed.
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
{
"description": "Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-validator.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write",
Expand Down
169 changes: 169 additions & 0 deletions hooks/lib/loop-common.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,46 @@ if ! validate_template_dir "$TEMPLATE_DIR" 2>/dev/null; then
echo "Warning: Template directory validation failed. Using inline fallbacks." >&2
fi

# Stop an active loop by renaming state.md to <prefix>-state.md
# This preserves the state file for manual inspection/restart if needed
# The prefix indicates why the loop was stopped:
# - completed: Normal completion (Codex said COMPLETE)
# - stopped: Stagnation/circuit breaker (Codex said STOP, or max iterations)
# - unexpected: Error conditions (corruption, legacy state, branch change)
# - cancelled: User manually cancelled
# Usage: stop_loop "$STATE_FILE" "completed|stopped|unexpected|cancelled"
stop_loop() {
local state_file="$1"
local prefix="${2:-stopped}" # Default to "stopped" for backward compatibility
if [[ -f "$state_file" ]]; then
local new_file="${state_file%state.md}${prefix}-state.md"
mv "$state_file" "$new_file" 2>/dev/null || rm -f "$state_file"
fi
}

# Check if HEAD is a descendant of start_commit
# Returns 0 if HEAD is descendant (valid), 1 if not (user checked out older branch)
# Usage: check_start_commit_ancestry "$START_COMMIT"
check_start_commit_ancestry() {
local start_commit="$1"

# If no start_commit or not in a git repo, skip check
if [[ -z "$start_commit" ]]; then
return 0
fi

if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
return 0
fi

# Check if start_commit is an ancestor of HEAD
if git merge-base --is-ancestor "$start_commit" HEAD 2>/dev/null; then
return 0
else
return 1
fi
}

# Find the most recent active loop directory
# Only checks the newest directory - older directories are ignored even if they have state.md
# This prevents "zombie" loops from being revived after abnormal exits
Expand DownExpand Up@@ -62,6 +102,135 @@ to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# Normalize a path: remove double slashes, resolve . and .. components
# Usage: _normalize_path "/home//user/../project/./src"
# Output: "/home/project/src"
# Note: This is a helper for get_relative_path, defined at top level for zsh compatibility
_normalize_path() {
# Enable shwordsplit for zsh (localoptions makes it local to this function)
[[ -n "$ZSH_VERSION" ]] && setopt localoptions shwordsplit

local path="$1"
# Remove trailing slash
path="${path%/}"
# Remove double slashes
while [[ "$path" == *//* ]]; do
path="${path//\/\//\/}"
done
# Resolve . and .. components by iterating over path parts
local result="" part old_ifs="$IFS"
IFS='/'
set -f # Disable glob expansion
for part in ${path#/}; do
if [[ -z "$part" ]] || [[ "$part" == "." ]]; then
continue
elif [[ "$part" == ".." ]]; then
# Remove last component from result
result="${result%/*}"
else
result="$result/$part"
fi
done
set +f # Re-enable glob expansion
IFS="$old_ifs"
if [[ -z "$result" ]]; then
echo "/"
else
echo "$result"
fi
}

# Compute relative path from base to target (portable across Linux/macOS/BSD)
# Usage: get_relative_path "/base/dir" "/base/dir/sub/file.txt"
# Output: "sub/file.txt"
get_relative_path() {
# Enable 0-indexed arrays in zsh for bash compatibility
[[ -n "$ZSH_VERSION" ]] && setopt localoptions ksharrays

local base="$1"
local target="$2"

# Normalize paths (resolve symlinks, remove trailing slashes)
local base_real target_real
base_real=$(cd "$base" 2>/dev/null && pwd -P) || base_real="$base"
target_real=$(realpath "$target" 2>/dev/null) || target_real="$target"

# Try GNU realpath --relative-to (Linux)
if realpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi

# Try grealpath (macOS with coreutils)
if command -v grealpath &>/dev/null; then
if grealpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi
fi

# Fallback: compute relative path in bash/zsh (works for inside/outside base)
if [[ "$base_real" == /* ]] && [[ "$target_real" == /* ]]; then
local base_clean target_clean

# Normalize paths using top-level helper (for zsh compatibility)
base_clean=$(_normalize_path "$base_real")
target_clean=$(_normalize_path "$target_real")
[[ -z "$base_clean" ]] && base_clean="/"
[[ -z "$target_clean" ]] && target_clean="/"

if [[ "$base_clean" == "$target_clean" ]]; then
echo "."
return 0
fi

local base_trim target_trim
base_trim="${base_clean#/}"
target_trim="${target_clean#/}"

# Split paths into arrays (bash/zsh compatible using awk)
# This avoids read -a which is bash-specific
local base_parts target_parts rel_parts
base_parts=$(echo "$base_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')
target_parts=$(echo "$target_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')

# Convert to arrays using while read (portable)
local -a base_arr target_arr rel_arr
while IFS= read -r part; do
[[ -n "$part" ]] && base_arr+=("$part")
done <<< "$base_parts"
while IFS= read -r part; do
[[ -n "$part" ]] && target_arr+=("$part")
done <<< "$target_parts"

# Find common prefix length
local i=0
while [[ $i -lt ${#base_arr[@]} ]] && [[ $i -lt ${#target_arr[@]} ]] && \
[[ "${base_arr[$i]}" == "${target_arr[$i]}" ]]; do
((i++))
done

# Add ".." for each remaining base component
local j
for ((j=i; j<${#base_arr[@]}; j++)); do
rel_arr+=("..")
done
# Add remaining target components
for ((j=i; j<${#target_arr[@]}; j++)); do
rel_arr+=("${target_arr[$j]}")
done

if (( ${#rel_arr[@]} == 0 )); then
echo "."
else
local IFS='/'
echo "${rel_arr[*]}"
fi
return 0
fi

# Cannot compute relative path, return basename as last resort
basename "$target"
Comment on lines +229 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect external plan files correctly in get_relative_path

On macOS (or any environment without GNU realpath --relative-to and without grealpath), get_relative_path falls back to basename when the target isn’t under the base directory. Downstream logic in setup-rlcr-loop.sh and loop-plan-validator.sh treats “outside repo” as PLAN_FILE_REL starting with ../, so returning just the basename misclassifies an external plan file as inside the repo. That means the --commit-plan-file outside-repo conflict won’t trigger, and it can even incorrectly “match” a tracked file at the repo root with the same basename. This breaks the intended Case 3 behavior on macOS. Consider returning a true relative path (e.g., via python -c 'os.path.relpath') or an absolute path and updating the inside/outside check accordingly.

Useful? React with 👍 / 👎.

}

# Check if a path (lowercase) matches a round file pattern
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos"
is_round_file_type() {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9cdc84d
Add --commit-plan-file option for plan file version control
SihaoLiu Jan 14, 2026
d977d3b
Add validation for plan file outside project with --commit-plan-file
SihaoLiu Jan 14, 2026
a05f080
Fix PROJECT_ROOT unbound variable and improve plan file modification …
SihaoLiu Jan 15, 2026
8fc5d01
Update version bump rules for feature branches vs main branch
SihaoLiu Jan 15, 2026
425b630
Fix plan file handling for all four cases (P1, P2, P3)
SihaoLiu Jan 15, 2026
a86bc04
Fix stop hook to follow Claude Code hooks spec
SihaoLiu Jan 15, 2026
9bc8214
Simplify plan file handling code
SihaoLiu Jan 15, 2026
9555c5c
Fix plan file handling edge cases (P1-P4)
SihaoLiu Jan 15, 2026
f112744
Move plan file validation to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
6d02655
Move pre-1.1.2 state file check to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
42009ed
Fix pre-1.1.2 loop handling and portable path resolution
SihaoLiu Jan 15, 2026
34da7fa
Fix Python fallback injection, test signing, and README Case 4
SihaoLiu Jan 15, 2026
bc0fb24
Improve get_relative_path to handle paths outside base directory
SihaoLiu Jan 15, 2026
6a18c15
Add pre-commit check to block git commit when plan file is staged
SihaoLiu Jan 15, 2026
5dc3a6a
Fix --commit-plan-file validation for monorepo subdirectories
SihaoLiu Jan 15, 2026
54147e1
Fix plan validator inside/outside repo check for monorepo subdirectories
SihaoLiu Jan 15, 2026
1706bbd
Fix git status/diff path matching for monorepo subdirectories
SihaoLiu Jan 15, 2026
eb32366
Fix regex escaping for plan file paths with + character
SihaoLiu Jan 15, 2026
00930e9
Add git ancestry check and prefixed state file termination
SihaoLiu Jan 15, 2026
9c075fe
Fix git commands to use git-relative path in plan validator
SihaoLiu Jan 15, 2026
4dd1ca2
Fix code review issues for monorepo and POSIX compatibility
SihaoLiu Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@
"name": "humanize",
"source": "./",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1"
"version": "1.1.2"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"name": "humanize",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1",
"version": "1.1.2",
"author": {
"name": "humania-org"
},
Expand Down
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ This is a Claude Code plugin that provides iterative development with Codex revi

# Humanize Project Rules
- Everything about this project, including but not limited to implementations, comments, tests and documentations should be in English. No Emoji or CJK char is allowed.
- **MANDATORY**: Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). This applies to ALL commits without exception - bug fixes, features, documentation changes, etc. Increment the patch version (e.g., 1.0.0 -> 1.0.1) for each commit.
- **Version Update Rules**:
- **On feature branches** (not `main` or `master`): No need to bump version on every commit. However, ensure the branch has an incremental version update compared to the default main branch before merging.
- **On default branch** (`main` or `master`): Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). Increment the patch version (e.g., 1.0.0 -> 1.0.1, 1.0.9 -> 1.1.0, 3.4.7 -> 4.0.0) for each commit.
- Every `git push` or `git commit` MUST confirm with user first, MUST NOT commit or push to remote without check with user.
- Version number must be in format of `X.Y.Z` where X/Y/Z is numeric number. Version MUST NOT include anything other than `X.Y.Z`. For example, a good version is `9.732.42`; Bad version examples (MUST NOT USE): `3.22.7-alpha` (extra "-alpha" string), `9.77.2 (2026-01-07)` (useless date/timestamp).
10 changes: 10 additions & 0 deletions .github/workflows/template-test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,13 +6,15 @@ on:
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'
pull_request:
branches: ['**']
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'

Expand DownExpand Up@@ -47,6 +49,14 @@ jobs:
chmod +x tests/test-template-references.sh
./tests/test-template-references.sh

- name: Run plan file handling tests
run: |
echo "========================================"
echo "Running plan file handling tests"
echo "========================================"
chmod +x tests/test-plan-file-handling.sh
./tests/test-plan-file-handling.sh

- name: Verify all hook scripts still work
run: |
echo "========================================"
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.1**
**Current Version: 1.1.2**

> Derived from the [GAAC (GitHub-as-a-Context)](https://github.com/SihaoLiu/gaac) project.

Expand DownExpand Up@@ -115,18 +115,26 @@ This provides a real-time dashboard showing:

- **Loop state**: Controlled solely by the presence of `.humanize-loop.local/*/state.md`
- **Resume**: Simply restart Claude Code in the same directory - the loop continues automatically
- **Cancel**: Remove the state file to stop the loop permanently
- **Cancel**: Rename the state file with a prefix to stop the loop

```bash
# Cancel the active loop
/humanize:cancel-rlcr-loop

# Or manually remove state file
rm .humanize-loop.local/*/state.md
# Or manually rename state file
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```

The loop directory with all summaries and review results is preserved for reference.

**State file prefixes indicate termination reason:**
- `completed-state.md` - Normal completion (Codex said COMPLETE)
- `stopped-state.md` - Stagnation/circuit breaker (Codex said STOP, or max iterations)
- `cancelled-state.md` - User manually cancelled
- `unexpected-state.md` - Error conditions (corruption, legacy state, branch change)

Any prefixed state file can be renamed back to `state.md` to manually restart the loop.

## Goal Tracker System

Humanize uses a **Goal Tracker** to prevent goal drift across iterations:
Expand DownExpand Up@@ -196,19 +204,53 @@ OPTIONS:
--codex-timeout <SECONDS>
Timeout for each Codex review in seconds (default: 5400)
--push-every-round Require git push after each round (default: commits stay local)
--commit-plan-file Include the plan file in commits (default: plan file stays uncommitted)
-h, --help Show help message
```

### Plan File Handling

The plan file behavior depends on the `--commit-plan-file` flag and whether the plan file is inside or outside the git repository:

#### Case 1: Inside repo with `--commit-plan-file`
- **Setup**: Plan file must be tracked (committed) AND clean (no uncommitted changes)
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing
- **Stop hook**: If plan file has uncommitted changes (dirty), allows stop with error message
- Plan file changes must be committed like any other file

#### Case 2: Outside repo with `--commit-plan-file`
- **Setup**: Early failure - this configuration is not allowed
- **Stop hook**: If detected (e.g., from manual state file edit), allows stop with conflict error

#### Case 3: Inside repo without `--commit-plan-file` (default)
- **Setup**: Plan file can be tracked or untracked, dirty or clean
- **UserPromptSubmit hook**: If plan file content differs from backup (or is missing), **blocks prompt** before processing with recovery options
- Accidental commits of the plan file are blocked with an error
- A backup of the original plan file is saved to `.humanize-loop.local/<timestamp>/plan-backup.md`

#### Case 4: Outside repo without `--commit-plan-file`
- **Setup**: No restrictions
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing with recovery options

**Key principle**: Plan file content changes are detected **before prompt processing** via the UserPromptSubmit hook. This blocks work from starting with a stale plan, giving the user a chance to restart the loop or restore the original plan.

## Prerequisites

Required tools:
**Required environment:**
- Git repository with at least one commit
- Plan file path must not contain spaces or special characters (`[ ] * ? { } | ( ) ^ $ \`)

**Required tools:**
- `codex` - OpenAI Codex CLI (for review)

Check if Codex is available:
```bash
codex --version
```

**Note on upgrading from pre-1.1.2:**
If you have an active RLCR loop started with a version before 1.1.2, the loop will be automatically terminated on the next stop attempt. Your work is preserved in `.humanize-loop.local/` - simply start a new loop with the updated plugin.

## Directory Structure

```
Expand DownExpand Up@@ -243,7 +285,8 @@ humanize/
When loop is active, creates: `.humanize-loop.local/<TIMESTAMP>/`

**Files Created**:
- `state.md` - Current round, config (YAML frontmatter)
- `state.md` - Current round, config (YAML frontmatter with `start_commit`, `commit_plan_file`, etc.)
- `plan-backup.md` - Backup copy of the original plan file at loop start
- `goal-tracker.md` - Immutable (goals/AC) + Mutable (active tasks, deferred, etc.)
- `round-N-prompt.md` - Instructions FROM Codex TO Claude
- `round-N-summary.md` - Work summary written BY Claude
Expand Down
8 changes: 6 additions & 2 deletions commands/cancel-rlcr-loop.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
description: "Cancel active RLCR loop"
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(rm .humanize-loop.local/*/state.md)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(for f in .humanize-loop.local/*/state.md*)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
hide-from-slash-command-tool: "true"
---

Expand All@@ -18,7 +18,11 @@ ls .humanize-loop.local/*/state.md 2>/dev/null || echo "NO_LOOP"

3. **If state file(s) found**:
- Read the state file to get the current round number
- Remove the state file(s) using: `rm .humanize-loop.local/*/state.md`
- Rename the state file(s) to cancelled-state.md:
```bash
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```
- Report: "Cancelled RLCR loop (was at round N of M)"

The loop directory with summaries and review results will be preserved for reference.
The cancelled-state.md file can be renamed back to state.md to manually restart the loop if needed.
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
{
"description": "Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-validator.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write",
Expand Down
169 changes: 169 additions & 0 deletions hooks/lib/loop-common.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,46 @@ if ! validate_template_dir "$TEMPLATE_DIR" 2>/dev/null; then
echo "Warning: Template directory validation failed. Using inline fallbacks." >&2
fi

# Stop an active loop by renaming state.md to <prefix>-state.md
# This preserves the state file for manual inspection/restart if needed
# The prefix indicates why the loop was stopped:
# - completed: Normal completion (Codex said COMPLETE)
# - stopped: Stagnation/circuit breaker (Codex said STOP, or max iterations)
# - unexpected: Error conditions (corruption, legacy state, branch change)
# - cancelled: User manually cancelled
# Usage: stop_loop "$STATE_FILE" "completed|stopped|unexpected|cancelled"
stop_loop() {
local state_file="$1"
local prefix="${2:-stopped}" # Default to "stopped" for backward compatibility
if [[ -f "$state_file" ]]; then
local new_file="${state_file%state.md}${prefix}-state.md"
mv "$state_file" "$new_file" 2>/dev/null || rm -f "$state_file"
fi
}

# Check if HEAD is a descendant of start_commit
# Returns 0 if HEAD is descendant (valid), 1 if not (user checked out older branch)
# Usage: check_start_commit_ancestry "$START_COMMIT"
check_start_commit_ancestry() {
local start_commit="$1"

# If no start_commit or not in a git repo, skip check
if [[ -z "$start_commit" ]]; then
return 0
fi

if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
return 0
fi

# Check if start_commit is an ancestor of HEAD
if git merge-base --is-ancestor "$start_commit" HEAD 2>/dev/null; then
return 0
else
return 1
fi
}

# Find the most recent active loop directory
# Only checks the newest directory - older directories are ignored even if they have state.md
# This prevents "zombie" loops from being revived after abnormal exits
Expand DownExpand Up@@ -62,6 +102,135 @@ to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# Normalize a path: remove double slashes, resolve . and .. components
# Usage: _normalize_path "/home//user/../project/./src"
# Output: "/home/project/src"
# Note: This is a helper for get_relative_path, defined at top level for zsh compatibility
_normalize_path() {
# Enable shwordsplit for zsh (localoptions makes it local to this function)
[[ -n "$ZSH_VERSION" ]] && setopt localoptions shwordsplit

local path="$1"
# Remove trailing slash
path="${path%/}"
# Remove double slashes
while [[ "$path" == *//* ]]; do
path="${path//\/\//\/}"
done
# Resolve . and .. components by iterating over path parts
local result="" part old_ifs="$IFS"
IFS='/'
set -f # Disable glob expansion
for part in ${path#/}; do
if [[ -z "$part" ]] || [[ "$part" == "." ]]; then
continue
elif [[ "$part" == ".." ]]; then
# Remove last component from result
result="${result%/*}"
else
result="$result/$part"
fi
done
set +f # Re-enable glob expansion
IFS="$old_ifs"
if [[ -z "$result" ]]; then
echo "/"
else
echo "$result"
fi
}

# Compute relative path from base to target (portable across Linux/macOS/BSD)
# Usage: get_relative_path "/base/dir" "/base/dir/sub/file.txt"
# Output: "sub/file.txt"
get_relative_path() {
# Enable 0-indexed arrays in zsh for bash compatibility
[[ -n "$ZSH_VERSION" ]] && setopt localoptions ksharrays

local base="$1"
local target="$2"

# Normalize paths (resolve symlinks, remove trailing slashes)
local base_real target_real
base_real=$(cd "$base" 2>/dev/null && pwd -P) || base_real="$base"
target_real=$(realpath "$target" 2>/dev/null) || target_real="$target"

# Try GNU realpath --relative-to (Linux)
if realpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi

# Try grealpath (macOS with coreutils)
if command -v grealpath &>/dev/null; then
if grealpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi
fi

# Fallback: compute relative path in bash/zsh (works for inside/outside base)
if [[ "$base_real" == /* ]] && [[ "$target_real" == /* ]]; then
local base_clean target_clean

# Normalize paths using top-level helper (for zsh compatibility)
base_clean=$(_normalize_path "$base_real")
target_clean=$(_normalize_path "$target_real")
[[ -z "$base_clean" ]] && base_clean="/"
[[ -z "$target_clean" ]] && target_clean="/"

if [[ "$base_clean" == "$target_clean" ]]; then
echo "."
return 0
fi

local base_trim target_trim
base_trim="${base_clean#/}"
target_trim="${target_clean#/}"

# Split paths into arrays (bash/zsh compatible using awk)
# This avoids read -a which is bash-specific
local base_parts target_parts rel_parts
base_parts=$(echo "$base_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')
target_parts=$(echo "$target_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')

# Convert to arrays using while read (portable)
local -a base_arr target_arr rel_arr
while IFS= read -r part; do
[[ -n "$part" ]] && base_arr+=("$part")
done <<< "$base_parts"
while IFS= read -r part; do
[[ -n "$part" ]] && target_arr+=("$part")
done <<< "$target_parts"

# Find common prefix length
local i=0
while [[ $i -lt ${#base_arr[@]} ]] && [[ $i -lt ${#target_arr[@]} ]] && \
[[ "${base_arr[$i]}" == "${target_arr[$i]}" ]]; do
((i++))
done

# Add ".." for each remaining base component
local j
for ((j=i; j<${#base_arr[@]}; j++)); do
rel_arr+=("..")
done
# Add remaining target components
for ((j=i; j<${#target_arr[@]}; j++)); do
rel_arr+=("${target_arr[$j]}")
done

if (( ${#rel_arr[@]} == 0 )); then
echo "."
else
local IFS='/'
echo "${rel_arr[*]}"
fi
return 0
fi

# Cannot compute relative path, return basename as last resort
basename "$target"
Comment on lines +229 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect external plan files correctly in get_relative_path

On macOS (or any environment without GNU realpath --relative-to and without grealpath), get_relative_path falls back to basename when the target isn’t under the base directory. Downstream logic in setup-rlcr-loop.sh and loop-plan-validator.sh treats “outside repo” as PLAN_FILE_REL starting with ../, so returning just the basename misclassifies an external plan file as inside the repo. That means the --commit-plan-file outside-repo conflict won’t trigger, and it can even incorrectly “match” a tracked file at the repo root with the same basename. This breaks the intended Case 3 behavior on macOS. Consider returning a true relative path (e.g., via python -c 'os.path.relpath') or an absolute path and updating the inside/outside check accordingly.

Useful? React with 👍 / 👎.

}

# Check if a path (lowercase) matches a round file pattern
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos"
is_round_file_type() {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9cdc84d
Add --commit-plan-file option for plan file version control
SihaoLiu Jan 14, 2026
d977d3b
Add validation for plan file outside project with --commit-plan-file
SihaoLiu Jan 14, 2026
a05f080
Fix PROJECT_ROOT unbound variable and improve plan file modification …
SihaoLiu Jan 15, 2026
8fc5d01
Update version bump rules for feature branches vs main branch
SihaoLiu Jan 15, 2026
425b630
Fix plan file handling for all four cases (P1, P2, P3)
SihaoLiu Jan 15, 2026
a86bc04
Fix stop hook to follow Claude Code hooks spec
SihaoLiu Jan 15, 2026
9bc8214
Simplify plan file handling code
SihaoLiu Jan 15, 2026
9555c5c
Fix plan file handling edge cases (P1-P4)
SihaoLiu Jan 15, 2026
f112744
Move plan file validation to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
6d02655
Move pre-1.1.2 state file check to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
42009ed
Fix pre-1.1.2 loop handling and portable path resolution
SihaoLiu Jan 15, 2026
34da7fa
Fix Python fallback injection, test signing, and README Case 4
SihaoLiu Jan 15, 2026
bc0fb24
Improve get_relative_path to handle paths outside base directory
SihaoLiu Jan 15, 2026
6a18c15
Add pre-commit check to block git commit when plan file is staged
SihaoLiu Jan 15, 2026
5dc3a6a
Fix --commit-plan-file validation for monorepo subdirectories
SihaoLiu Jan 15, 2026
54147e1
Fix plan validator inside/outside repo check for monorepo subdirectories
SihaoLiu Jan 15, 2026
1706bbd
Fix git status/diff path matching for monorepo subdirectories
SihaoLiu Jan 15, 2026
eb32366
Fix regex escaping for plan file paths with + character
SihaoLiu Jan 15, 2026
00930e9
Add git ancestry check and prefixed state file termination
SihaoLiu Jan 15, 2026
9c075fe
Fix git commands to use git-relative path in plan validator
SihaoLiu Jan 15, 2026
4dd1ca2
Fix code review issues for monorepo and POSIX compatibility
SihaoLiu Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@
"name": "humanize",
"source": "./",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1"
"version": "1.1.2"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"name": "humanize",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1",
"version": "1.1.2",
"author": {
"name": "humania-org"
},
Expand Down
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ This is a Claude Code plugin that provides iterative development with Codex revi

# Humanize Project Rules
- Everything about this project, including but not limited to implementations, comments, tests and documentations should be in English. No Emoji or CJK char is allowed.
- **MANDATORY**: Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). This applies to ALL commits without exception - bug fixes, features, documentation changes, etc. Increment the patch version (e.g., 1.0.0 -> 1.0.1) for each commit.
- **Version Update Rules**:
- **On feature branches** (not `main` or `master`): No need to bump version on every commit. However, ensure the branch has an incremental version update compared to the default main branch before merging.
- **On default branch** (`main` or `master`): Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). Increment the patch version (e.g., 1.0.0 -> 1.0.1, 1.0.9 -> 1.1.0, 3.4.7 -> 4.0.0) for each commit.
- Every `git push` or `git commit` MUST confirm with user first, MUST NOT commit or push to remote without check with user.
- Version number must be in format of `X.Y.Z` where X/Y/Z is numeric number. Version MUST NOT include anything other than `X.Y.Z`. For example, a good version is `9.732.42`; Bad version examples (MUST NOT USE): `3.22.7-alpha` (extra "-alpha" string), `9.77.2 (2026-01-07)` (useless date/timestamp).
10 changes: 10 additions & 0 deletions .github/workflows/template-test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,13 +6,15 @@ on:
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'
pull_request:
branches: ['**']
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'

Expand DownExpand Up@@ -47,6 +49,14 @@ jobs:
chmod +x tests/test-template-references.sh
./tests/test-template-references.sh

- name: Run plan file handling tests
run: |
echo "========================================"
echo "Running plan file handling tests"
echo "========================================"
chmod +x tests/test-plan-file-handling.sh
./tests/test-plan-file-handling.sh

- name: Verify all hook scripts still work
run: |
echo "========================================"
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.1**
**Current Version: 1.1.2**

> Derived from the [GAAC (GitHub-as-a-Context)](https://github.com/SihaoLiu/gaac) project.

Expand DownExpand Up@@ -115,18 +115,26 @@ This provides a real-time dashboard showing:

- **Loop state**: Controlled solely by the presence of `.humanize-loop.local/*/state.md`
- **Resume**: Simply restart Claude Code in the same directory - the loop continues automatically
- **Cancel**: Remove the state file to stop the loop permanently
- **Cancel**: Rename the state file with a prefix to stop the loop

```bash
# Cancel the active loop
/humanize:cancel-rlcr-loop

# Or manually remove state file
rm .humanize-loop.local/*/state.md
# Or manually rename state file
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```

The loop directory with all summaries and review results is preserved for reference.

**State file prefixes indicate termination reason:**
- `completed-state.md` - Normal completion (Codex said COMPLETE)
- `stopped-state.md` - Stagnation/circuit breaker (Codex said STOP, or max iterations)
- `cancelled-state.md` - User manually cancelled
- `unexpected-state.md` - Error conditions (corruption, legacy state, branch change)

Any prefixed state file can be renamed back to `state.md` to manually restart the loop.

## Goal Tracker System

Humanize uses a **Goal Tracker** to prevent goal drift across iterations:
Expand DownExpand Up@@ -196,19 +204,53 @@ OPTIONS:
--codex-timeout <SECONDS>
Timeout for each Codex review in seconds (default: 5400)
--push-every-round Require git push after each round (default: commits stay local)
--commit-plan-file Include the plan file in commits (default: plan file stays uncommitted)
-h, --help Show help message
```

### Plan File Handling

The plan file behavior depends on the `--commit-plan-file` flag and whether the plan file is inside or outside the git repository:

#### Case 1: Inside repo with `--commit-plan-file`
- **Setup**: Plan file must be tracked (committed) AND clean (no uncommitted changes)
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing
- **Stop hook**: If plan file has uncommitted changes (dirty), allows stop with error message
- Plan file changes must be committed like any other file

#### Case 2: Outside repo with `--commit-plan-file`
- **Setup**: Early failure - this configuration is not allowed
- **Stop hook**: If detected (e.g., from manual state file edit), allows stop with conflict error

#### Case 3: Inside repo without `--commit-plan-file` (default)
- **Setup**: Plan file can be tracked or untracked, dirty or clean
- **UserPromptSubmit hook**: If plan file content differs from backup (or is missing), **blocks prompt** before processing with recovery options
- Accidental commits of the plan file are blocked with an error
- A backup of the original plan file is saved to `.humanize-loop.local/<timestamp>/plan-backup.md`

#### Case 4: Outside repo without `--commit-plan-file`
- **Setup**: No restrictions
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing with recovery options

**Key principle**: Plan file content changes are detected **before prompt processing** via the UserPromptSubmit hook. This blocks work from starting with a stale plan, giving the user a chance to restart the loop or restore the original plan.

## Prerequisites

Required tools:
**Required environment:**
- Git repository with at least one commit
- Plan file path must not contain spaces or special characters (`[ ] * ? { } | ( ) ^ $ \`)

**Required tools:**
- `codex` - OpenAI Codex CLI (for review)

Check if Codex is available:
```bash
codex --version
```

**Note on upgrading from pre-1.1.2:**
If you have an active RLCR loop started with a version before 1.1.2, the loop will be automatically terminated on the next stop attempt. Your work is preserved in `.humanize-loop.local/` - simply start a new loop with the updated plugin.

## Directory Structure

```
Expand DownExpand Up@@ -243,7 +285,8 @@ humanize/
When loop is active, creates: `.humanize-loop.local/<TIMESTAMP>/`

**Files Created**:
- `state.md` - Current round, config (YAML frontmatter)
- `state.md` - Current round, config (YAML frontmatter with `start_commit`, `commit_plan_file`, etc.)
- `plan-backup.md` - Backup copy of the original plan file at loop start
- `goal-tracker.md` - Immutable (goals/AC) + Mutable (active tasks, deferred, etc.)
- `round-N-prompt.md` - Instructions FROM Codex TO Claude
- `round-N-summary.md` - Work summary written BY Claude
Expand Down
8 changes: 6 additions & 2 deletions commands/cancel-rlcr-loop.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
description: "Cancel active RLCR loop"
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(rm .humanize-loop.local/*/state.md)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(for f in .humanize-loop.local/*/state.md*)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
hide-from-slash-command-tool: "true"
---

Expand All@@ -18,7 +18,11 @@ ls .humanize-loop.local/*/state.md 2>/dev/null || echo "NO_LOOP"

3. **If state file(s) found**:
- Read the state file to get the current round number
- Remove the state file(s) using: `rm .humanize-loop.local/*/state.md`
- Rename the state file(s) to cancelled-state.md:
```bash
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```
- Report: "Cancelled RLCR loop (was at round N of M)"

The loop directory with summaries and review results will be preserved for reference.
The cancelled-state.md file can be renamed back to state.md to manually restart the loop if needed.
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
{
"description": "Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-validator.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write",
Expand Down
169 changes: 169 additions & 0 deletions hooks/lib/loop-common.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,46 @@ if ! validate_template_dir "$TEMPLATE_DIR" 2>/dev/null; then
echo "Warning: Template directory validation failed. Using inline fallbacks." >&2
fi

# Stop an active loop by renaming state.md to <prefix>-state.md
# This preserves the state file for manual inspection/restart if needed
# The prefix indicates why the loop was stopped:
# - completed: Normal completion (Codex said COMPLETE)
# - stopped: Stagnation/circuit breaker (Codex said STOP, or max iterations)
# - unexpected: Error conditions (corruption, legacy state, branch change)
# - cancelled: User manually cancelled
# Usage: stop_loop "$STATE_FILE" "completed|stopped|unexpected|cancelled"
stop_loop() {
local state_file="$1"
local prefix="${2:-stopped}" # Default to "stopped" for backward compatibility
if [[ -f "$state_file" ]]; then
local new_file="${state_file%state.md}${prefix}-state.md"
mv "$state_file" "$new_file" 2>/dev/null || rm -f "$state_file"
fi
}

# Check if HEAD is a descendant of start_commit
# Returns 0 if HEAD is descendant (valid), 1 if not (user checked out older branch)
# Usage: check_start_commit_ancestry "$START_COMMIT"
check_start_commit_ancestry() {
local start_commit="$1"

# If no start_commit or not in a git repo, skip check
if [[ -z "$start_commit" ]]; then
return 0
fi

if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
return 0
fi

# Check if start_commit is an ancestor of HEAD
if git merge-base --is-ancestor "$start_commit" HEAD 2>/dev/null; then
return 0
else
return 1
fi
}

# Find the most recent active loop directory
# Only checks the newest directory - older directories are ignored even if they have state.md
# This prevents "zombie" loops from being revived after abnormal exits
Expand DownExpand Up@@ -62,6 +102,135 @@ to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# Normalize a path: remove double slashes, resolve . and .. components
# Usage: _normalize_path "/home//user/../project/./src"
# Output: "/home/project/src"
# Note: This is a helper for get_relative_path, defined at top level for zsh compatibility
_normalize_path() {
# Enable shwordsplit for zsh (localoptions makes it local to this function)
[[ -n "$ZSH_VERSION" ]] && setopt localoptions shwordsplit

local path="$1"
# Remove trailing slash
path="${path%/}"
# Remove double slashes
while [[ "$path" == *//* ]]; do
path="${path//\/\//\/}"
done
# Resolve . and .. components by iterating over path parts
local result="" part old_ifs="$IFS"
IFS='/'
set -f # Disable glob expansion
for part in ${path#/}; do
if [[ -z "$part" ]] || [[ "$part" == "." ]]; then
continue
elif [[ "$part" == ".." ]]; then
# Remove last component from result
result="${result%/*}"
else
result="$result/$part"
fi
done
set +f # Re-enable glob expansion
IFS="$old_ifs"
if [[ -z "$result" ]]; then
echo "/"
else
echo "$result"
fi
}

# Compute relative path from base to target (portable across Linux/macOS/BSD)
# Usage: get_relative_path "/base/dir" "/base/dir/sub/file.txt"
# Output: "sub/file.txt"
get_relative_path() {
# Enable 0-indexed arrays in zsh for bash compatibility
[[ -n "$ZSH_VERSION" ]] && setopt localoptions ksharrays

local base="$1"
local target="$2"

# Normalize paths (resolve symlinks, remove trailing slashes)
local base_real target_real
base_real=$(cd "$base" 2>/dev/null && pwd -P) || base_real="$base"
target_real=$(realpath "$target" 2>/dev/null) || target_real="$target"

# Try GNU realpath --relative-to (Linux)
if realpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi

# Try grealpath (macOS with coreutils)
if command -v grealpath &>/dev/null; then
if grealpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi
fi

# Fallback: compute relative path in bash/zsh (works for inside/outside base)
if [[ "$base_real" == /* ]] && [[ "$target_real" == /* ]]; then
local base_clean target_clean

# Normalize paths using top-level helper (for zsh compatibility)
base_clean=$(_normalize_path "$base_real")
target_clean=$(_normalize_path "$target_real")
[[ -z "$base_clean" ]] && base_clean="/"
[[ -z "$target_clean" ]] && target_clean="/"

if [[ "$base_clean" == "$target_clean" ]]; then
echo "."
return 0
fi

local base_trim target_trim
base_trim="${base_clean#/}"
target_trim="${target_clean#/}"

# Split paths into arrays (bash/zsh compatible using awk)
# This avoids read -a which is bash-specific
local base_parts target_parts rel_parts
base_parts=$(echo "$base_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')
target_parts=$(echo "$target_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')

# Convert to arrays using while read (portable)
local -a base_arr target_arr rel_arr
while IFS= read -r part; do
[[ -n "$part" ]] && base_arr+=("$part")
done <<< "$base_parts"
while IFS= read -r part; do
[[ -n "$part" ]] && target_arr+=("$part")
done <<< "$target_parts"

# Find common prefix length
local i=0
while [[ $i -lt ${#base_arr[@]} ]] && [[ $i -lt ${#target_arr[@]} ]] && \
[[ "${base_arr[$i]}" == "${target_arr[$i]}" ]]; do
((i++))
done

# Add ".." for each remaining base component
local j
for ((j=i; j<${#base_arr[@]}; j++)); do
rel_arr+=("..")
done
# Add remaining target components
for ((j=i; j<${#target_arr[@]}; j++)); do
rel_arr+=("${target_arr[$j]}")
done

if (( ${#rel_arr[@]} == 0 )); then
echo "."
else
local IFS='/'
echo "${rel_arr[*]}"
fi
return 0
fi

# Cannot compute relative path, return basename as last resort
basename "$target"
Comment on lines +229 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect external plan files correctly in get_relative_path

On macOS (or any environment without GNU realpath --relative-to and without grealpath), get_relative_path falls back to basename when the target isn’t under the base directory. Downstream logic in setup-rlcr-loop.sh and loop-plan-validator.sh treats “outside repo” as PLAN_FILE_REL starting with ../, so returning just the basename misclassifies an external plan file as inside the repo. That means the --commit-plan-file outside-repo conflict won’t trigger, and it can even incorrectly “match” a tracked file at the repo root with the same basename. This breaks the intended Case 3 behavior on macOS. Consider returning a true relative path (e.g., via python -c 'os.path.relpath') or an absolute path and updating the inside/outside check accordingly.

Useful? React with 👍 / 👎.

}

# Check if a path (lowercase) matches a round file pattern
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos"
is_round_file_type() {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9cdc84d
Add --commit-plan-file option for plan file version control
SihaoLiu Jan 14, 2026
d977d3b
Add validation for plan file outside project with --commit-plan-file
SihaoLiu Jan 14, 2026
a05f080
Fix PROJECT_ROOT unbound variable and improve plan file modification …
SihaoLiu Jan 15, 2026
8fc5d01
Update version bump rules for feature branches vs main branch
SihaoLiu Jan 15, 2026
425b630
Fix plan file handling for all four cases (P1, P2, P3)
SihaoLiu Jan 15, 2026
a86bc04
Fix stop hook to follow Claude Code hooks spec
SihaoLiu Jan 15, 2026
9bc8214
Simplify plan file handling code
SihaoLiu Jan 15, 2026
9555c5c
Fix plan file handling edge cases (P1-P4)
SihaoLiu Jan 15, 2026
f112744
Move plan file validation to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
6d02655
Move pre-1.1.2 state file check to UserPromptSubmit hook
SihaoLiu Jan 15, 2026
42009ed
Fix pre-1.1.2 loop handling and portable path resolution
SihaoLiu Jan 15, 2026
34da7fa
Fix Python fallback injection, test signing, and README Case 4
SihaoLiu Jan 15, 2026
bc0fb24
Improve get_relative_path to handle paths outside base directory
SihaoLiu Jan 15, 2026
6a18c15
Add pre-commit check to block git commit when plan file is staged
SihaoLiu Jan 15, 2026
5dc3a6a
Fix --commit-plan-file validation for monorepo subdirectories
SihaoLiu Jan 15, 2026
54147e1
Fix plan validator inside/outside repo check for monorepo subdirectories
SihaoLiu Jan 15, 2026
1706bbd
Fix git status/diff path matching for monorepo subdirectories
SihaoLiu Jan 15, 2026
eb32366
Fix regex escaping for plan file paths with + character
SihaoLiu Jan 15, 2026
00930e9
Add git ancestry check and prefixed state file termination
SihaoLiu Jan 15, 2026
9c075fe
Fix git commands to use git-relative path in plan validator
SihaoLiu Jan 15, 2026
4dd1ca2
Fix code review issues for monorepo and POSIX compatibility
SihaoLiu Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@
"name": "humanize",
"source": "./",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1"
"version": "1.1.2"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"name": "humanize",
"description": "Humanize - An iterative development plugin that uses Codex to review Claude's work. Creates a feedback loop where Claude implements plans and Codex independently reviews progress, ensuring quality through continuous refinement.",
"version": "1.1.1",
"version": "1.1.2",
"author": {
"name": "humania-org"
},
Expand Down
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ This is a Claude Code plugin that provides iterative development with Codex revi

# Humanize Project Rules
- Everything about this project, including but not limited to implementations, comments, tests and documentations should be in English. No Emoji or CJK char is allowed.
- **MANDATORY**: Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). This applies to ALL commits without exception - bug fixes, features, documentation changes, etc. Increment the patch version (e.g., 1.0.0 -> 1.0.1) for each commit.
- **Version Update Rules**:
- **On feature branches** (not `main` or `master`): No need to bump version on every commit. However, ensure the branch has an incremental version update compared to the default main branch before merging.
- **On default branch** (`main` or `master`): Every commit MUST include a version bump in `.claude-plugin/plugin.json` and `README.md` (the "Current Version" line). Increment the patch version (e.g., 1.0.0 -> 1.0.1, 1.0.9 -> 1.1.0, 3.4.7 -> 4.0.0) for each commit.
- Every `git push` or `git commit` MUST confirm with user first, MUST NOT commit or push to remote without check with user.
- Version number must be in format of `X.Y.Z` where X/Y/Z is numeric number. Version MUST NOT include anything other than `X.Y.Z`. For example, a good version is `9.732.42`; Bad version examples (MUST NOT USE): `3.22.7-alpha` (extra "-alpha" string), `9.77.2 (2026-01-07)` (useless date/timestamp).
10 changes: 10 additions & 0 deletions .github/workflows/template-test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,13 +6,15 @@ on:
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'
pull_request:
branches: ['**']
paths:
- 'prompt-template/**'
- 'hooks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/template-test.yml'

Expand DownExpand Up@@ -47,6 +49,14 @@ jobs:
chmod +x tests/test-template-references.sh
./tests/test-template-references.sh

- name: Run plan file handling tests
run: |
echo "========================================"
echo "Running plan file handling tests"
echo "========================================"
chmod +x tests/test-plan-file-handling.sh
./tests/test-plan-file-handling.sh

- name: Verify all hook scripts still work
run: |
echo "========================================"
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.1**
**Current Version: 1.1.2**

> Derived from the [GAAC (GitHub-as-a-Context)](https://github.com/SihaoLiu/gaac) project.

Expand DownExpand Up@@ -115,18 +115,26 @@ This provides a real-time dashboard showing:

- **Loop state**: Controlled solely by the presence of `.humanize-loop.local/*/state.md`
- **Resume**: Simply restart Claude Code in the same directory - the loop continues automatically
- **Cancel**: Remove the state file to stop the loop permanently
- **Cancel**: Rename the state file with a prefix to stop the loop

```bash
# Cancel the active loop
/humanize:cancel-rlcr-loop

# Or manually remove state file
rm .humanize-loop.local/*/state.md
# Or manually rename state file
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```

The loop directory with all summaries and review results is preserved for reference.

**State file prefixes indicate termination reason:**
- `completed-state.md` - Normal completion (Codex said COMPLETE)
- `stopped-state.md` - Stagnation/circuit breaker (Codex said STOP, or max iterations)
- `cancelled-state.md` - User manually cancelled
- `unexpected-state.md` - Error conditions (corruption, legacy state, branch change)

Any prefixed state file can be renamed back to `state.md` to manually restart the loop.

## Goal Tracker System

Humanize uses a **Goal Tracker** to prevent goal drift across iterations:
Expand DownExpand Up@@ -196,19 +204,53 @@ OPTIONS:
--codex-timeout <SECONDS>
Timeout for each Codex review in seconds (default: 5400)
--push-every-round Require git push after each round (default: commits stay local)
--commit-plan-file Include the plan file in commits (default: plan file stays uncommitted)
-h, --help Show help message
```

### Plan File Handling

The plan file behavior depends on the `--commit-plan-file` flag and whether the plan file is inside or outside the git repository:

#### Case 1: Inside repo with `--commit-plan-file`
- **Setup**: Plan file must be tracked (committed) AND clean (no uncommitted changes)
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing
- **Stop hook**: If plan file has uncommitted changes (dirty), allows stop with error message
- Plan file changes must be committed like any other file

#### Case 2: Outside repo with `--commit-plan-file`
- **Setup**: Early failure - this configuration is not allowed
- **Stop hook**: If detected (e.g., from manual state file edit), allows stop with conflict error

#### Case 3: Inside repo without `--commit-plan-file` (default)
- **Setup**: Plan file can be tracked or untracked, dirty or clean
- **UserPromptSubmit hook**: If plan file content differs from backup (or is missing), **blocks prompt** before processing with recovery options
- Accidental commits of the plan file are blocked with an error
- A backup of the original plan file is saved to `.humanize-loop.local/<timestamp>/plan-backup.md`

#### Case 4: Outside repo without `--commit-plan-file`
- **Setup**: No restrictions
- **UserPromptSubmit hook**: If plan file content differs from backup, **blocks prompt** before processing with recovery options

**Key principle**: Plan file content changes are detected **before prompt processing** via the UserPromptSubmit hook. This blocks work from starting with a stale plan, giving the user a chance to restart the loop or restore the original plan.

## Prerequisites

Required tools:
**Required environment:**
- Git repository with at least one commit
- Plan file path must not contain spaces or special characters (`[ ] * ? { } | ( ) ^ $ \`)

**Required tools:**
- `codex` - OpenAI Codex CLI (for review)

Check if Codex is available:
```bash
codex --version
```

**Note on upgrading from pre-1.1.2:**
If you have an active RLCR loop started with a version before 1.1.2, the loop will be automatically terminated on the next stop attempt. Your work is preserved in `.humanize-loop.local/` - simply start a new loop with the updated plugin.

## Directory Structure

```
Expand DownExpand Up@@ -243,7 +285,8 @@ humanize/
When loop is active, creates: `.humanize-loop.local/<TIMESTAMP>/`

**Files Created**:
- `state.md` - Current round, config (YAML frontmatter)
- `state.md` - Current round, config (YAML frontmatter with `start_commit`, `commit_plan_file`, etc.)
- `plan-backup.md` - Backup copy of the original plan file at loop start
- `goal-tracker.md` - Immutable (goals/AC) + Mutable (active tasks, deferred, etc.)
- `round-N-prompt.md` - Instructions FROM Codex TO Claude
- `round-N-summary.md` - Work summary written BY Claude
Expand Down
8 changes: 6 additions & 2 deletions commands/cancel-rlcr-loop.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
description: "Cancel active RLCR loop"
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(rm .humanize-loop.local/*/state.md)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
allowed-tools: ["Bash(ls .humanize-loop.local/*/state.md:*)", "Bash(for f in .humanize-loop.local/*/state.md*)", "Bash(cat .humanize-loop.local/*/state.md)", "Read"]
hide-from-slash-command-tool: "true"
---

Expand All@@ -18,7 +18,11 @@ ls .humanize-loop.local/*/state.md 2>/dev/null || echo "NO_LOOP"

3. **If state file(s) found**:
- Read the state file to get the current round number
- Remove the state file(s) using: `rm .humanize-loop.local/*/state.md`
- Rename the state file(s) to cancelled-state.md:
```bash
for f in .humanize-loop.local/*/state.md; do mv "$f" "${f%state.md}cancelled-state.md"; done
```
- Report: "Cancelled RLCR loop (was at round N of M)"

The loop directory with summaries and review results will be preserved for reference.
The cancelled-state.md file can be renamed back to state.md to manually restart the loop if needed.
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
{
"description": "Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-validator.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write",
Expand Down
169 changes: 169 additions & 0 deletions hooks/lib/loop-common.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,46 @@ if ! validate_template_dir "$TEMPLATE_DIR" 2>/dev/null; then
echo "Warning: Template directory validation failed. Using inline fallbacks." >&2
fi

# Stop an active loop by renaming state.md to <prefix>-state.md
# This preserves the state file for manual inspection/restart if needed
# The prefix indicates why the loop was stopped:
# - completed: Normal completion (Codex said COMPLETE)
# - stopped: Stagnation/circuit breaker (Codex said STOP, or max iterations)
# - unexpected: Error conditions (corruption, legacy state, branch change)
# - cancelled: User manually cancelled
# Usage: stop_loop "$STATE_FILE" "completed|stopped|unexpected|cancelled"
stop_loop() {
local state_file="$1"
local prefix="${2:-stopped}" # Default to "stopped" for backward compatibility
if [[ -f "$state_file" ]]; then
local new_file="${state_file%state.md}${prefix}-state.md"
mv "$state_file" "$new_file" 2>/dev/null || rm -f "$state_file"
fi
}

# Check if HEAD is a descendant of start_commit
# Returns 0 if HEAD is descendant (valid), 1 if not (user checked out older branch)
# Usage: check_start_commit_ancestry "$START_COMMIT"
check_start_commit_ancestry() {
local start_commit="$1"

# If no start_commit or not in a git repo, skip check
if [[ -z "$start_commit" ]]; then
return 0
fi

if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
return 0
fi

# Check if start_commit is an ancestor of HEAD
if git merge-base --is-ancestor "$start_commit" HEAD 2>/dev/null; then
return 0
else
return 1
fi
}

# Find the most recent active loop directory
# Only checks the newest directory - older directories are ignored even if they have state.md
# This prevents "zombie" loops from being revived after abnormal exits
Expand DownExpand Up@@ -62,6 +102,135 @@ to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# Normalize a path: remove double slashes, resolve . and .. components
# Usage: _normalize_path "/home//user/../project/./src"
# Output: "/home/project/src"
# Note: This is a helper for get_relative_path, defined at top level for zsh compatibility
_normalize_path() {
# Enable shwordsplit for zsh (localoptions makes it local to this function)
[[ -n "$ZSH_VERSION" ]] && setopt localoptions shwordsplit

local path="$1"
# Remove trailing slash
path="${path%/}"
# Remove double slashes
while [[ "$path" == *//* ]]; do
path="${path//\/\//\/}"
done
# Resolve . and .. components by iterating over path parts
local result="" part old_ifs="$IFS"
IFS='/'
set -f # Disable glob expansion
for part in ${path#/}; do
if [[ -z "$part" ]] || [[ "$part" == "." ]]; then
continue
elif [[ "$part" == ".." ]]; then
# Remove last component from result
result="${result%/*}"
else
result="$result/$part"
fi
done
set +f # Re-enable glob expansion
IFS="$old_ifs"
if [[ -z "$result" ]]; then
echo "/"
else
echo "$result"
fi
}

# Compute relative path from base to target (portable across Linux/macOS/BSD)
# Usage: get_relative_path "/base/dir" "/base/dir/sub/file.txt"
# Output: "sub/file.txt"
get_relative_path() {
# Enable 0-indexed arrays in zsh for bash compatibility
[[ -n "$ZSH_VERSION" ]] && setopt localoptions ksharrays

local base="$1"
local target="$2"

# Normalize paths (resolve symlinks, remove trailing slashes)
local base_real target_real
base_real=$(cd "$base" 2>/dev/null && pwd -P) || base_real="$base"
target_real=$(realpath "$target" 2>/dev/null) || target_real="$target"

# Try GNU realpath --relative-to (Linux)
if realpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi

# Try grealpath (macOS with coreutils)
if command -v grealpath &>/dev/null; then
if grealpath --relative-to="$base_real" "$target_real" 2>/dev/null; then
return 0
fi
fi

# Fallback: compute relative path in bash/zsh (works for inside/outside base)
if [[ "$base_real" == /* ]] && [[ "$target_real" == /* ]]; then
local base_clean target_clean

# Normalize paths using top-level helper (for zsh compatibility)
base_clean=$(_normalize_path "$base_real")
target_clean=$(_normalize_path "$target_real")
[[ -z "$base_clean" ]] && base_clean="/"
[[ -z "$target_clean" ]] && target_clean="/"

if [[ "$base_clean" == "$target_clean" ]]; then
echo "."
return 0
fi

local base_trim target_trim
base_trim="${base_clean#/}"
target_trim="${target_clean#/}"

# Split paths into arrays (bash/zsh compatible using awk)
# This avoids read -a which is bash-specific
local base_parts target_parts rel_parts
base_parts=$(echo "$base_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')
target_parts=$(echo "$target_trim" | awk -F'/' '{ for(i=1;i<=NF;i++) if($i!="") print $i }')

# Convert to arrays using while read (portable)
local -a base_arr target_arr rel_arr
while IFS= read -r part; do
[[ -n "$part" ]] && base_arr+=("$part")
done <<< "$base_parts"
while IFS= read -r part; do
[[ -n "$part" ]] && target_arr+=("$part")
done <<< "$target_parts"

# Find common prefix length
local i=0
while [[ $i -lt ${#base_arr[@]} ]] && [[ $i -lt ${#target_arr[@]} ]] && \
[[ "${base_arr[$i]}" == "${target_arr[$i]}" ]]; do
((i++))
done

# Add ".." for each remaining base component
local j
for ((j=i; j<${#base_arr[@]}; j++)); do
rel_arr+=("..")
done
# Add remaining target components
for ((j=i; j<${#target_arr[@]}; j++)); do
rel_arr+=("${target_arr[$j]}")
done

if (( ${#rel_arr[@]} == 0 )); then
echo "."
else
local IFS='/'
echo "${rel_arr[*]}"
fi
return 0
fi

# Cannot compute relative path, return basename as last resort
basename "$target"
Comment on lines +229 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect external plan files correctly in get_relative_path

On macOS (or any environment without GNU realpath --relative-to and without grealpath), get_relative_path falls back to basename when the target isn’t under the base directory. Downstream logic in setup-rlcr-loop.sh and loop-plan-validator.sh treats “outside repo” as PLAN_FILE_REL starting with ../, so returning just the basename misclassifies an external plan file as inside the repo. That means the --commit-plan-file outside-repo conflict won’t trigger, and it can even incorrectly “match” a tracked file at the repo root with the same basename. This breaks the intended Case 3 behavior on macOS. Consider returning a true relative path (e.g., via python -c 'os.path.relpath') or an absolute path and updating the inside/outside check accordingly.

Useful? React with 👍 / 👎.

}

# Check if a path (lowercase) matches a round file pattern
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos"
is_round_file_type() {
Expand Down
Loading