Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e2b3064
Refactor: General code quality improvements (Phase 1-5)
SihaoLiu Jan 16, 2026
ea5751d
Refactor: Complete Phase 3-4 blockers from Codex review
SihaoLiu Jan 16, 2026
fed8b43
Refactor: Complete all remaining blockers from Round 1 Codex review
SihaoLiu Jan 16, 2026
23c7871
Refactor: Fix git timeout fail-open patterns and complete state parsi…
SihaoLiu Jan 16, 2026
4bb3fe1
Refactor: Fix remaining fail-closed patterns and HTML comment parsing
SihaoLiu Jan 16, 2026
7de1696
Refactor: Complete fail-closed handling and fix test harness signing
SihaoLiu Jan 16, 2026
d00d86b
Test: Unset CLAUDE_PROJECT_DIR in test-plan-file-validation.sh
SihaoLiu Jan 16, 2026
d4e714a
Fix: Section-specific placeholder detection in stop hook
SihaoLiu Jan 16, 2026
3f45375
Fix: Section-scoped placeholder detection using awk extraction
SihaoLiu Jan 16, 2026
3d7451f
Fix: Fail-closed on git status timeout in stop hook
SihaoLiu Jan 16, 2026
19a8d87
Fix: ANSI escape code handling in test runner output parsing
SihaoLiu Jan 16, 2026
b45cd4a
Fix: Portable ANSI escape code handling and add regression tests
SihaoLiu Jan 16, 2026
1ed1828
Refactor: Convert test-error-scenarios.sh to assertion-based tests
SihaoLiu Jan 16, 2026
1820595
Fix: Remove incorrect set -e toggle in test-error-scenarios.sh Test 6
SihaoLiu Jan 16, 2026
536f74c
Feature: Add allowlist for required RLCR round files
SihaoLiu Jan 16, 2026
562538f
Feature: Tighten allowlist and add validator tests
SihaoLiu Jan 16, 2026
1fea22e
Fix: Fail-closed bash allowlist using full path match
SihaoLiu Jan 16, 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 number Diff line number Diff 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.4"
"version": "1.1.5"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff 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.4",
"version": "1.1.5",
"author": {
"name": "humania-org"
},
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Humanize

**Current Version: 1.1.4**
**Current Version: 1.1.5**

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

Expand Down
14 changes: 9 additions & 5 deletions hooks/check-todos-from-transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
Helper script to check for incomplete todos from Claude Code transcript.

Reads the transcript JSONL file and finds the most recent TodoWrite tool call.
Returns exit code 0 if all todos are completed (or no todos exist).
Returns exit code 1 if there are incomplete todos, with details on stderr.

Exit codes:
0 - All todos are completed (or no todos exist)
1 - There are incomplete todos (details on stdout)
2 - Parse error reading hook input JSON

Usage:
echo '{"transcript_path": "/path/to/transcript.jsonl"}' | python3 check-todos-from-transcript.py
Expand Down Expand Up @@ -88,9 +91,10 @@ def main():
# Read hook input from stdin
try:
hook_input = json.load(sys.stdin)
except json.JSONDecodeError:
# No valid input, assume no todos
sys.exit(0)
except json.JSONDecodeError as e:
# Parse error - exit with code 2
print(f"PARSE_ERROR: {e}", file=sys.stderr)
sys.exit(2)

transcript_path = hook_input.get("transcript_path", "")
if not transcript_path:
Expand Down
104 changes: 103 additions & 1 deletion hooks/lib/loop-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,41 @@
# - loop-bash-validator.sh
#

# ========================================
# Constants
# ========================================

# State file field names
readonly FIELD_PLAN_TRACKED="plan_tracked"
readonly FIELD_START_BRANCH="start_branch"
readonly FIELD_PLAN_FILE="plan_file"
readonly FIELD_CURRENT_ROUND="current_round"
readonly FIELD_MAX_ITERATIONS="max_iterations"
readonly FIELD_PUSH_EVERY_ROUND="push_every_round"
readonly FIELD_CODEX_MODEL="codex_model"
readonly FIELD_CODEX_EFFORT="codex_effort"
readonly FIELD_CODEX_TIMEOUT="codex_timeout"

# Codex review markers
readonly MARKER_COMPLETE="COMPLETE"
readonly MARKER_STOP="STOP"

# Exit reasons (used with end_loop function)
# complete - Codex confirmed all goals achieved (normal success)
# cancel - User cancelled with /cancel-rlcr-loop
# maxiter - Reached maximum iterations limit
# stop - Codex triggered circuit breaker (stagnation detected)
# unexpected - System error or invalid state (e.g., corrupted state file)
readonly EXIT_COMPLETE="complete"
readonly EXIT_CANCEL="cancel"
readonly EXIT_MAXITER="maxiter"
readonly EXIT_STOP="stop"
readonly EXIT_UNEXPECTED="unexpected"

# ========================================
# Library Setup
# ========================================

# Source template loader
LOOP_COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$LOOP_COMMON_DIR/template-loader.sh"
Expand Down Expand Up @@ -45,18 +80,62 @@ find_active_loop() {

# Extract current round number from state.md
# Outputs the round number to stdout, defaults to 0
# Note: For full state parsing, use parse_state_file() instead
get_current_round() {
local state_file="$1"

local frontmatter
frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$state_file" 2>/dev/null || echo "")

local current_round
current_round=$(echo "$frontmatter" | grep '^current_round:' | sed 's/current_round: *//' | tr -d ' ')
current_round=$(echo "$frontmatter" | grep "^${FIELD_CURRENT_ROUND}:" | sed "s/${FIELD_CURRENT_ROUND}: *//" | tr -d ' ')

echo "${current_round:-0}"
}

# Parse state file frontmatter and set variables
# Usage: parse_state_file "$STATE_FILE"
# Sets the following variables (caller must declare them):
# STATE_FRONTMATTER - raw frontmatter content
# STATE_PLAN_TRACKED - "true" or "false"
# STATE_START_BRANCH - branch name
# STATE_PLAN_FILE - plan file path
# STATE_CURRENT_ROUND - current round number
# STATE_MAX_ITERATIONS - max iterations
# STATE_PUSH_EVERY_ROUND - "true" or "false"
# STATE_CODEX_MODEL - codex model name
# STATE_CODEX_EFFORT - codex effort level
# STATE_CODEX_TIMEOUT - codex timeout in seconds
# Returns: 0 on success, 1 if file not found
parse_state_file() {
local state_file="$1"

if [[ ! -f "$state_file" ]]; then
return 1
fi

STATE_FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$state_file" 2>/dev/null || echo "")

# Parse fields with consistent quote handling
# Legacy quote-stripping kept for backward compatibility with older state files
STATE_PLAN_TRACKED=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_PLAN_TRACKED}:" | sed "s/${FIELD_PLAN_TRACKED}: *//" | tr -d ' ' || true)
STATE_START_BRANCH=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_START_BRANCH}:" | sed "s/${FIELD_START_BRANCH}: *//; s/^\"//; s/\"\$//" || true)
STATE_PLAN_FILE=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_PLAN_FILE}:" | sed "s/${FIELD_PLAN_FILE}: *//; s/^\"//; s/\"\$//" || true)
STATE_CURRENT_ROUND=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_CURRENT_ROUND}:" | sed "s/${FIELD_CURRENT_ROUND}: *//" | tr -d ' ' || true)
STATE_MAX_ITERATIONS=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_MAX_ITERATIONS}:" | sed "s/${FIELD_MAX_ITERATIONS}: *//" | tr -d ' ' || true)
STATE_PUSH_EVERY_ROUND=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_PUSH_EVERY_ROUND}:" | sed "s/${FIELD_PUSH_EVERY_ROUND}: *//" | tr -d ' ' || true)
STATE_CODEX_MODEL=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_CODEX_MODEL}:" | sed "s/${FIELD_CODEX_MODEL}: *//" | tr -d ' ' || true)
STATE_CODEX_EFFORT=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_CODEX_EFFORT}:" | sed "s/${FIELD_CODEX_EFFORT}: *//" | tr -d ' ' || true)
STATE_CODEX_TIMEOUT=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_CODEX_TIMEOUT}:" | sed "s/${FIELD_CODEX_TIMEOUT}: *//" | tr -d ' ' || true)

# Apply defaults
STATE_CURRENT_ROUND="${STATE_CURRENT_ROUND:-0}"
STATE_MAX_ITERATIONS="${STATE_MAX_ITERATIONS:-10}"
STATE_PUSH_EVERY_ROUND="${STATE_PUSH_EVERY_ROUND:-false}"

return 0
}

# Convert a string to lowercase
to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
Expand All @@ -83,6 +162,29 @@ extract_round_number() {
echo "$filename_lower" | sed -n 's/.*round-\([0-9][0-9]*\)-\(summary\|prompt\|todos\)\.md$/\1/p'
}

# Check if a file is in the allowlist for the active loop
# Usage: is_allowlisted_file "$file_path" "$active_loop_dir"
# Returns: 0 if allowlisted, 1 otherwise
is_allowlisted_file() {
local file_path="$1"
local active_loop_dir="$2"

local allowlist=(
"round-1-todos.md"
"round-2-todos.md"
"round-0-summary.md"
"round-1-summary.md"
)

for allowed in "${allowlist[@]}"; do
if [[ "$file_path" == "$active_loop_dir/$allowed" ]]; then
return 0
fi
done

return 1
}

# Standard message for blocking todos file access
# Usage: todos_blocked_message "Read|Write|Bash"
todos_blocked_message() {
Expand Down
15 changes: 15 additions & 0 deletions hooks/lib/template-loader.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@
# Template loading functions for RLCR loop hooks
#
# This library provides functions to load and render prompt templates.
#
# Template Variable Syntax
# ========================
# Templates use {{VARIABLE_NAME}} syntax for placeholders.
# - Variable names: uppercase letters, numbers, underscores only
# - Example: {{PLAN_FILE}}, {{CURRENT_ROUND}}, {{GOAL_TRACKER_FILE}}
# - Single-pass substitution: {{VAR}} in a value will NOT be expanded
# - Missing variables: placeholder is kept as-is (e.g., {{UNDEFINED}})
#
# Available functions:
# - get_template_dir: Get path to template directory
# - load_template: Load a template file by name
# - render_template: Replace {{VAR}} placeholders with values
# - load_and_render: Load and render in one call
# - load_and_render_safe: Same as above but with fallback for missing templates
# - validate_template_dir: Check if template directory is valid
#

# Get the template directory path
Expand Down
17 changes: 13 additions & 4 deletions hooks/loop-bash-validator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,19 @@ if [[ -z "$ACTIVE_LOOP_DIR" ]]; then
exit 0
fi

CURRENT_ROUND=$(get_current_round "$ACTIVE_LOOP_DIR/state.md")
STATE_FILE="$ACTIVE_LOOP_DIR/state.md"

# Parse state file using shared function to get current round
parse_state_file "$STATE_FILE"
CURRENT_ROUND="$STATE_CURRENT_ROUND"

# ========================================
# Block Git Push When push_every_round is false
# ========================================
# Default behavior: commits stay local, no need to push to remote

PUSH_EVERY_ROUND=$(grep -E "^push_every_round:" "$STATE_FILE" 2>/dev/null | sed 's/push_every_round: *//' || echo "false")
# Note: parse_state_file was called above, STATE_* vars are available
PUSH_EVERY_ROUND="$STATE_PUSH_EVERY_ROUND"

if [[ "$PUSH_EVERY_ROUND" != "true" ]]; then
# Check if command is a git push command
Expand Down Expand Up @@ -130,8 +134,13 @@ fi
# ========================================

if command_modifies_file "$COMMAND_LOWER" "round-[0-9]+-todos\.md"; then
todos_blocked_message "Bash" >&2
exit 2
# Require full path to active loop dir to prevent same-basename bypass from different roots
ACTIVE_LOOP_DIR_LOWER=$(to_lower "$ACTIVE_LOOP_DIR")
ACTIVE_LOOP_DIR_ESCAPED=$(echo "$ACTIVE_LOOP_DIR_LOWER" | sed 's/[\\.*^$[(){}+?|]/\\&/g')
if ! echo "$COMMAND_LOWER" | grep -qE "${ACTIVE_LOOP_DIR_ESCAPED}/round-[12]-todos\.md"; then
todos_blocked_message "Bash" >&2
exit 2
fi
fi

exit 0
Loading