From 9b961a83365863d39d9c87a33f0ce0004cf0ed46 Mon Sep 17 00:00:00 2001 From: tastynoob <934348725@qq.com> Date: Sun, 15 Mar 2026 14:56:07 +0800 Subject: [PATCH] Harden RLCR against mainline drift --- hooks/check-todos-from-transcript.py | 29 +- hooks/lib/loop-common.sh | 235 +++++++++++++- hooks/loop-bash-validator.sh | 24 +- hooks/loop-codex-stop-hook.sh | 283 ++++++++++++++-- hooks/loop-edit-validator.sh | 63 +++- hooks/loop-read-validator.sh | 55 +++- hooks/loop-write-validator.sh | 77 +++-- .../block/finalize-contract-access.md | 7 + .../block/goal-tracker-modification.md | 29 +- prompt-template/block/mainline-drift-stop.md | 14 + .../block/mainline-verdict-missing.md | 13 + .../block/round-contract-bash-write.md | 7 + .../block/round-contract-missing.md | 13 + .../block/wrong-contract-location.md | 5 + prompt-template/claude/drift-replan-prompt.md | 68 ++++ .../claude/finalize-phase-prompt.md | 7 +- .../claude/finalize-phase-skipped-prompt.md | 7 +- .../claude/goal-tracker-update-request.md | 7 +- prompt-template/claude/next-round-prompt.md | 51 ++- .../claude/post-alignment-action-items.md | 1 + prompt-template/claude/review-phase-prompt.md | 37 ++- .../codex/full-alignment-review.md | 29 +- .../codex/goal-tracker-update-section.md | 13 +- prompt-template/codex/regular-review.md | 21 +- scripts/humanize.sh | 77 ++++- scripts/lib/monitor-common.sh | 49 ++- scripts/setup-rlcr-loop.sh | 304 +++++++++++++++--- .../test-goal-tracker-robustness.sh | 59 ++++ .../robustness/test-hook-system-robustness.sh | 192 ++++++++++- .../test-setup-scripts-robustness.sh | 64 ++++ .../robustness/test-state-file-robustness.sh | 49 +++ tests/test-agent-teams.sh | 57 ++++ tests/test-allowlist-validators.sh | 105 +++++- tests/test-finalize-phase.sh | 209 ++++++++++++ tests/test-plan-file-hooks.sh | 63 +++- tests/test-task-tag-routing.sh | 11 + tests/test-todo-checker.sh | 81 +++++ 37 files changed, 2242 insertions(+), 173 deletions(-) create mode 100644 prompt-template/block/finalize-contract-access.md create mode 100644 prompt-template/block/mainline-drift-stop.md create mode 100644 prompt-template/block/mainline-verdict-missing.md create mode 100644 prompt-template/block/round-contract-bash-write.md create mode 100644 prompt-template/block/round-contract-missing.md create mode 100644 prompt-template/block/wrong-contract-location.md create mode 100644 prompt-template/claude/drift-replan-prompt.md diff --git a/hooks/check-todos-from-transcript.py b/hooks/check-todos-from-transcript.py index af577a5c..31ec6e5e 100755 --- a/hooks/check-todos-from-transcript.py +++ b/hooks/check-todos-from-transcript.py @@ -15,11 +15,26 @@ echo '{"session_id": "...", "transcript_path": "/path/to/transcript.jsonl"}' | python3 check-todos-from-transcript.py """ import json +import re import sys from pathlib import Path from typing import List, Tuple +LANE_PREFIX_PATTERN = re.compile(r"^\s*\[(mainline|blocking|queued)\](?:\s|$)", re.IGNORECASE) + + +def classify_lane(*parts: str) -> str: + """Infer the task lane from content, defaulting to blocking for safety.""" + for part in parts: + if not part: + continue + match = LANE_PREFIX_PATTERN.match(part) + if match: + return match.group(1).lower() + return "blocking" + + def extract_tool_calls_from_entry(entry: dict) -> List[Tuple[str, dict]]: """ Extract tool calls from a transcript entry. @@ -92,10 +107,14 @@ def find_incomplete_todos_from_transcript(transcript_path: Path) -> List[dict]: status = todo.get("status", "") content = todo.get("content", "") if status != "completed": + lane = classify_lane(content) + if lane == "queued": + continue incomplete.append({ "status": status, "content": content, "source": "todo", + "lane": lane, }) return incomplete @@ -134,11 +153,15 @@ def find_incomplete_tasks_from_directory(session_id: str, tasks_base_dir: str = description = task.get("description", "") task_id = task_file.stem # Filename without .json content = subject or description or f"Task {task_id}" + lane = classify_lane(subject, description) + if lane == "queued": + continue incomplete.append({ "status": status, "content": content, "source": "task", "task_id": task_id, + "lane": lane, }) except (json.JSONDecodeError, OSError): # Skip malformed or unreadable task files @@ -184,11 +207,13 @@ def main(): status = item.get("status", "unknown") content = item.get("content", "") source = item.get("source", "unknown") + lane = item.get("lane", "blocking") + lane_marker = f"[{lane}]" if source == "task": task_id = item.get("task_id", "?") - output_lines.append(f" - [{status}] (Task #{task_id}) {content}") + output_lines.append(f" - [{status}] {lane_marker} (Task #{task_id}) {content}") else: - output_lines.append(f" - [{status}] {content}") + output_lines.append(f" - [{status}] {lane_marker} {content}") # Output marker and incomplete items both to stdout print("INCOMPLETE_TODOS") diff --git a/hooks/lib/loop-common.sh b/hooks/lib/loop-common.sh index 5151018f..b6bc2e5b 100755 --- a/hooks/lib/loop-common.sh +++ b/hooks/lib/loop-common.sh @@ -38,6 +38,17 @@ readonly FIELD_FULL_REVIEW_ROUND="full_review_round" readonly FIELD_ASK_CODEX_QUESTION="ask_codex_question" readonly FIELD_SESSION_ID="session_id" readonly FIELD_AGENT_TEAMS="agent_teams" +readonly FIELD_MAINLINE_STALL_COUNT="mainline_stall_count" +readonly FIELD_LAST_MAINLINE_VERDICT="last_mainline_verdict" +readonly FIELD_DRIFT_STATUS="drift_status" + +readonly MAINLINE_VERDICT_ADVANCED="advanced" +readonly MAINLINE_VERDICT_STALLED="stalled" +readonly MAINLINE_VERDICT_REGRESSED="regressed" +readonly MAINLINE_VERDICT_UNKNOWN="unknown" + +readonly DRIFT_STATUS_NORMAL="normal" +readonly DRIFT_STATUS_REPLAN_REQUIRED="replan_required" # Default Codex configuration (single source of truth - all scripts reference this) # Scripts can pre-set DEFAULT_CODEX_MODEL/DEFAULT_CODEX_EFFORT before sourcing to override. @@ -364,6 +375,9 @@ _parse_state_fields() { STATE_ASK_CODEX_QUESTION=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_ASK_CODEX_QUESTION}:" | sed "s/${FIELD_ASK_CODEX_QUESTION}: *//" | tr -d ' ' || true) STATE_SESSION_ID=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_SESSION_ID}:" | sed "s/${FIELD_SESSION_ID}: *//" || true) STATE_AGENT_TEAMS=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_AGENT_TEAMS}:" | sed "s/${FIELD_AGENT_TEAMS}: *//" | tr -d ' ' || true) + STATE_MAINLINE_STALL_COUNT=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_MAINLINE_STALL_COUNT}:" | sed "s/${FIELD_MAINLINE_STALL_COUNT}: *//" | tr -d ' ' || true) + STATE_LAST_MAINLINE_VERDICT=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_LAST_MAINLINE_VERDICT}:" | sed "s/${FIELD_LAST_MAINLINE_VERDICT}: *//" | tr -d ' ' || true) + STATE_DRIFT_STATUS=$(echo "$STATE_FRONTMATTER" | grep "^${FIELD_DRIFT_STATUS}:" | sed "s/${FIELD_DRIFT_STATUS}: *//" | tr -d ' ' || true) } # Parse state file frontmatter and set variables (tolerant mode with defaults) @@ -384,6 +398,9 @@ _parse_state_fields() { # STATE_FULL_REVIEW_ROUND - interval for Full Alignment Check (default: 5) # STATE_ASK_CODEX_QUESTION - "true" or "false" (v1.6.5+) # STATE_AGENT_TEAMS - "true" or "false" +# STATE_MAINLINE_STALL_COUNT - consecutive stalled/regressed implementation rounds +# STATE_LAST_MAINLINE_VERDICT - advanced/stalled/regressed/unknown +# STATE_DRIFT_STATUS - normal/replan_required # Returns: 0 on success, 1 if file not found # Note: For strict validation, use parse_state_file_strict() instead parse_state_file() { @@ -406,6 +423,9 @@ parse_state_file() { STATE_FULL_REVIEW_ROUND="${STATE_FULL_REVIEW_ROUND:-5}" STATE_ASK_CODEX_QUESTION="${STATE_ASK_CODEX_QUESTION:-true}" STATE_AGENT_TEAMS="${STATE_AGENT_TEAMS:-false}" + STATE_MAINLINE_STALL_COUNT="${STATE_MAINLINE_STALL_COUNT:-0}" + STATE_LAST_MAINLINE_VERDICT="${STATE_LAST_MAINLINE_VERDICT:-$MAINLINE_VERDICT_UNKNOWN}" + STATE_DRIFT_STATUS="${STATE_DRIFT_STATUS:-$DRIFT_STATUS_NORMAL}" # STATE_REVIEW_STARTED left as-is (empty if missing, to allow schema validation) return 0 @@ -481,10 +501,116 @@ parse_state_file_strict() { STATE_FULL_REVIEW_ROUND="${STATE_FULL_REVIEW_ROUND:-5}" STATE_ASK_CODEX_QUESTION="${STATE_ASK_CODEX_QUESTION:-true}" STATE_AGENT_TEAMS="${STATE_AGENT_TEAMS:-false}" + STATE_MAINLINE_STALL_COUNT="${STATE_MAINLINE_STALL_COUNT:-0}" + STATE_LAST_MAINLINE_VERDICT="${STATE_LAST_MAINLINE_VERDICT:-$MAINLINE_VERDICT_UNKNOWN}" + STATE_DRIFT_STATUS="${STATE_DRIFT_STATUS:-$DRIFT_STATUS_NORMAL}" return 0 } +# Normalize mainline progress verdict to a safe enum. +# Usage: normalize_mainline_progress_verdict "ADVANCED" +normalize_mainline_progress_verdict() { + local verdict_lower + verdict_lower=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') + + case "$verdict_lower" in + "$MAINLINE_VERDICT_ADVANCED"|"$MAINLINE_VERDICT_STALLED"|"$MAINLINE_VERDICT_REGRESSED") + echo "$verdict_lower" + ;; + *) + echo "$MAINLINE_VERDICT_UNKNOWN" + ;; + esac +} + +# Normalize drift status to a safe enum. +# Usage: normalize_drift_status "replan_required" +normalize_drift_status() { + local status_lower + status_lower=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') + + case "$status_lower" in + "$DRIFT_STATUS_REPLAN_REQUIRED") + echo "$DRIFT_STATUS_REPLAN_REQUIRED" + ;; + *) + echo "$DRIFT_STATUS_NORMAL" + ;; + esac +} + +# Extract "Mainline Progress Verdict" from Codex review content. +# Outputs one of: advanced, stalled, regressed, unknown +# Usage: extract_mainline_progress_verdict "$review_content" +extract_mainline_progress_verdict() { + local review_content="$1" + local verdict_line + local verdict_value + + verdict_line=$(printf '%s\n' "$review_content" | grep -Ei 'Mainline Progress Verdict:[[:space:]]*(ADVANCED|STALLED|REGRESSED)([^A-Za-z]|$)' | tail -1 || true) + if [[ -z "$verdict_line" ]]; then + echo "$MAINLINE_VERDICT_UNKNOWN" + return + fi + + verdict_value=$(printf '%s\n' "$verdict_line" | sed -E 's/.*Mainline Progress Verdict:[[:space:]]*(ADVANCED|STALLED|REGRESSED).*/\1/I') + normalize_mainline_progress_verdict "$verdict_value" +} + +# Upsert simple YAML frontmatter fields in a state file. +# Values must not contain newlines. +# Usage: upsert_state_fields "/path/to/state.md" "field=value" "other=value" +upsert_state_fields() { + local state_file="$1" + shift + + local temp_file="${state_file}.tmp.$$" + + awk -v assignments="$*" ' + BEGIN { + count = split(assignments, pairs, " "); + for (i = 1; i <= count; i++) { + split(pairs[i], kv, "="); + keys[kv[1]] = kv[2]; + order[i] = kv[1]; + } + separator_count = 0; + } + { + if ($0 == "---") { + separator_count++; + if (separator_count == 2) { + for (i = 1; i <= count; i++) { + key = order[i]; + if (!(key in seen)) { + print key ": " keys[key]; + seen[key] = 1; + } + } + } + print; + next; + } + + handled = 0; + for (i = 1; i <= count; i++) { + key = order[i]; + if ($0 ~ ("^" key ":")) { + print key ": " keys[key]; + seen[key] = 1; + handled = 1; + break; + } + } + + if (!handled) { + print; + } + } + ' "$state_file" > "$temp_file" && mv "$temp_file" "$state_file" +} + # Detect review issues from codex review log file # Returns: # 0 - issues found (caller should continue review loop) @@ -562,7 +688,7 @@ to_lower() { } # Check if a path (lowercase) matches a round file pattern -# Usage: is_round_file "$lowercase_path" "summary|prompt|todos" +# Usage: is_round_file "$lowercase_path" "summary|prompt|todos|contract" is_round_file_type() { local path_lower="$1" local file_type="$2" @@ -579,7 +705,7 @@ extract_round_number() { filename_lower=$(to_lower "$filename") # Use sed for portable regex extraction (works in both bash and zsh) - echo "$filename_lower" | sed -n 's/.*round-\([0-9][0-9]*\)-\(summary\|prompt\|todos\)\.md$/\1/p' + echo "$filename_lower" | sed -n 's/.*round-\([0-9][0-9]*\)-\(summary\|prompt\|todos\|contract\)\.md$/\1/p' } # Check if a file is in the allowlist for the active loop @@ -643,6 +769,21 @@ You cannot modify finalize-state.md. This file is managed by the loop system dur load_and_render_safe "$TEMPLATE_DIR" "block/finalize-state-file-modification.md" "$fallback" } +# Standard message for blocking round contract access during Finalize Phase +# Usage: finalize_contract_blocked_message "read" +finalize_contract_blocked_message() { + local action="$1" + local fallback="# Finalize Contract Access Blocked + +There is no active round contract during the Finalize Phase. + +Do not {{ACTION}} historical round contract files. +Use finalize-summary.md for finalize-only notes and goal-tracker.md for current state." + + load_and_render_safe "$TEMPLATE_DIR" "block/finalize-contract-access.md" "$fallback" \ + "ACTION=$action" +} + # Standard message for blocking summary file modifications via Bash # Usage: summary_bash_blocked_message "$correct_summary_path" summary_bash_blocked_message() { @@ -671,6 +812,79 @@ is_goal_tracker_path() { echo "$path_lower" | grep -qE 'goal-tracker\.md$' } +# Extract the immutable section from a goal-tracker content stream. +# Supports both current trackers (with --- separator) and older trackers +# that jump directly from IMMUTABLE SECTION to MUTABLE SECTION. +extract_goal_tracker_immutable_from_stream() { + awk ' + /^## IMMUTABLE SECTION[[:space:]]*$/ { capture=1 } + capture && /^## MUTABLE SECTION[[:space:]]*$/ { exit } + capture && /^---[[:space:]]*$/ { exit } + capture { print } + ' +} + +# Extract the immutable section from an on-disk goal-tracker file. +# Usage: extract_goal_tracker_immutable_from_file "/path/to/goal-tracker.md" +extract_goal_tracker_immutable_from_file() { + local tracker_file="$1" + if [[ ! -f "$tracker_file" ]]; then + return 1 + fi + extract_goal_tracker_immutable_from_stream < "$tracker_file" +} + +# Extract the immutable section from an in-memory goal-tracker string. +# Usage: extract_goal_tracker_immutable_from_text "$content" +extract_goal_tracker_immutable_from_text() { + local tracker_content="$1" + printf '%s' "$tracker_content" | extract_goal_tracker_immutable_from_stream +} + +# Check whether a proposed goal-tracker update preserves the immutable section. +# Usage: goal_tracker_mutable_update_allowed "/path/to/current.md" "$new_content" +goal_tracker_mutable_update_allowed() { + local tracker_file="$1" + local updated_content="$2" + + local current_immutable="" + local updated_immutable="" + current_immutable=$(extract_goal_tracker_immutable_from_file "$tracker_file" 2>/dev/null || true) + updated_immutable=$(extract_goal_tracker_immutable_from_text "$updated_content" 2>/dev/null || true) + + [[ -n "$current_immutable" ]] || return 1 + [[ "$current_immutable" == "$updated_immutable" ]] +} + +# Render the post-edit contents for a literal Edit operation. +# Returns non-zero if the edit preview cannot be produced. +# Usage: preview_edit_result "/path/to/file" "$old_string" "$new_string" "true|false" +preview_edit_result() { + local file_path="$1" + local old_string="$2" + local new_string="$3" + local replace_all="${4:-false}" + + command -v perl >/dev/null 2>&1 || return 1 + + FILE_PATH="$file_path" \ + OLD_STRING="$old_string" \ + NEW_STRING="$new_string" \ + REPLACE_ALL="$replace_all" \ + perl -0pe ' + BEGIN { + $old = $ENV{"OLD_STRING"}; + $new = $ENV{"NEW_STRING"}; + $replace_all = $ENV{"REPLACE_ALL"} eq "true"; + } + if ($replace_all) { + s/\Q$old\E/$new/g; + } else { + s/\Q$old\E/$new/; + } + ' "$file_path" +} + # Check if a path (lowercase) targets state.md is_state_file_path() { local path_lower="$1" @@ -1275,17 +1489,24 @@ command_modifies_file() { } # Standard message for blocking goal-tracker modifications after Round 0 -# Usage: goal_tracker_blocked_message "$current_round" "$summary_file_path" +# Usage: goal_tracker_blocked_message "$current_round" "$correct_goal_tracker_path" goal_tracker_blocked_message() { local current_round="$1" - local summary_file="$2" - local fallback="# Goal Tracker Modification Blocked (Round {{CURRENT_ROUND}}) + local correct_path="$2" + local fallback="# Goal Tracker Update Blocked (Round {{CURRENT_ROUND}}) + +After Round 0, you may update only the **MUTABLE SECTION** of the active goal tracker. + +Use Write or Edit on: {{CORRECT_PATH}} -After Round 0, only Codex can modify the Goal Tracker. Include a Goal Tracker Update Request in your summary: {{SUMMARY_FILE}}" +Rules: +- Keep the **IMMUTABLE SECTION** unchanged +- Do not modify `goal-tracker.md` via Bash +- Do not write to an old loop session's tracker" load_and_render_safe "$TEMPLATE_DIR" "block/goal-tracker-modification.md" "$fallback" \ "CURRENT_ROUND=$current_round" \ - "SUMMARY_FILE=$summary_file" + "CORRECT_PATH=$correct_path" } # End the loop by renaming state.md to indicate exit reason diff --git a/hooks/loop-bash-validator.sh b/hooks/loop-bash-validator.sh index 948612e1..7a5fdec1 100755 --- a/hooks/loop-bash-validator.sh +++ b/hooks/loop-bash-validator.sh @@ -6,7 +6,7 @@ # - cat/echo/printf > file.md (redirection) # - tee file.md # - sed -i file.md (in-place edit) -# - goal-tracker.md modifications after Round 0 +# - goal-tracker.md modifications via Bash # - PR loop state.md modifications # - PR loop read-only file modifications (pr-comment, prompt, codex-prompt, etc.) # @@ -359,12 +359,11 @@ fi # Round > 0: prompt to put request in summary if command_modifies_file "$COMMAND_LOWER" "goal-tracker\.md"; then + GOAL_TRACKER_PATH="$ACTIVE_LOOP_DIR/goal-tracker.md" if [[ "$CURRENT_ROUND" -eq 0 ]]; then - GOAL_TRACKER_PATH="$ACTIVE_LOOP_DIR/goal-tracker.md" goal_tracker_bash_blocked_message "$GOAL_TRACKER_PATH" >&2 else - SUMMARY_FILE="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-summary.md" - goal_tracker_blocked_message "$CURRENT_ROUND" "$SUMMARY_FILE" >&2 + goal_tracker_blocked_message "$CURRENT_ROUND" "$GOAL_TRACKER_PATH" >&2 fi exit 2 fi @@ -390,6 +389,23 @@ if command_modifies_file "$COMMAND_LOWER" "round-[0-9]+-summary\.md"; then exit 2 fi +# ======================================== +# Block Round Contract File Modifications (All Rounds) +# ======================================== +# Round contracts should be written using Write or Edit tools so round scoping +# stays aligned with the current loop state. + +if command_modifies_file "$COMMAND_LOWER" "round-[0-9]+-contract\.md"; then + CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-contract.md" + FALLBACK="# Round Contract Bash Write Blocked + +Do not use Bash commands to modify round contract files. +Use the Write or Edit tool instead: {{CORRECT_PATH}}" + load_and_render_safe "$TEMPLATE_DIR" "block/round-contract-bash-write.md" "$FALLBACK" \ + "CORRECT_PATH=$CORRECT_PATH" >&2 + exit 2 +fi + # ======================================== # Block Todos File Modifications (All Rounds) # ======================================== diff --git a/hooks/loop-codex-stop-hook.sh b/hooks/loop-codex-stop-hook.sh index 25142818..95783918 100755 --- a/hooks/loop-codex-stop-hook.sh +++ b/hooks/loop-codex-stop-hook.sh @@ -148,6 +148,9 @@ fi if [[ "$BITLESSON_ALLOW_EMPTY_NONE" != "true" && "$BITLESSON_ALLOW_EMPTY_NONE" != "false" ]]; then BITLESSON_ALLOW_EMPTY_NONE="true" fi +MAINLINE_STALL_COUNT="${STATE_MAINLINE_STALL_COUNT:-0}" +LAST_MAINLINE_VERDICT="${STATE_LAST_MAINLINE_VERDICT:-$MAINLINE_VERDICT_UNKNOWN}" +DRIFT_STATUS="${STATE_DRIFT_STATUS:-$DRIFT_STATUS_NORMAL}" # Re-validate Codex Model and Effort for YAML safety (in case state.md was manually edited) # Use same validation patterns as setup-rlcr-loop.sh if [[ ! "$CODEX_EXEC_MODEL" =~ ^[a-zA-Z0-9._-]+$ ]]; then @@ -189,6 +192,13 @@ if [[ ! "$MAX_ITERATIONS" =~ ^[0-9]+$ ]]; then MAX_ITERATIONS=42 fi +if [[ ! "$MAINLINE_STALL_COUNT" =~ ^[0-9]+$ ]]; then + echo "Warning: Invalid mainline_stall_count '$MAINLINE_STALL_COUNT', defaulting to 0" >&2 + MAINLINE_STALL_COUNT=0 +fi +LAST_MAINLINE_VERDICT=$(normalize_mainline_progress_verdict "$LAST_MAINLINE_VERDICT") +DRIFT_STATUS=$(normalize_drift_status "$DRIFT_STATUS") + # ======================================== # Quick-check 0: Schema Validation (v1.1.2+ fields) # ======================================== @@ -682,8 +692,10 @@ fi # In Finalize Phase, expect finalize-summary.md instead of round-N-summary.md if [[ "$IS_FINALIZE_PHASE" == "true" ]]; then SUMMARY_FILE="$LOOP_DIR/finalize-summary.md" + ROUND_CONTRACT_FILE="" else SUMMARY_FILE="$LOOP_DIR/round-${CURRENT_ROUND}-summary.md" + ROUND_CONTRACT_FILE="$LOOP_DIR/round-${CURRENT_ROUND}-contract.md" fi if [[ ! -f "$SUMMARY_FILE" ]]; then @@ -713,6 +725,36 @@ Please write your work summary to: {{SUMMARY_FILE}}" exit 0 fi +# Check Round Contract Exists +# ======================================== + +if [[ "$IS_FINALIZE_PHASE" != "true" ]]; then + if [[ ! -f "$ROUND_CONTRACT_FILE" ]]; then + FALLBACK="# Round Contract Missing + +Before trying to exit, write the current round contract to: {{ROUND_CONTRACT_FILE}} + +The round contract must restate: +- The single mainline objective for this round +- The target ACs +- Which side issues are truly blocking +- Which side issues are queued and out of scope +- The success criteria for this round" + REASON=$(load_and_render_safe "$TEMPLATE_DIR" "block/round-contract-missing.md" "$FALLBACK" \ + "ROUND_CONTRACT_FILE=$ROUND_CONTRACT_FILE") + + jq -n \ + --arg reason "$REASON" \ + --arg msg "Loop: Round contract missing for round $CURRENT_ROUND" \ + '{ + "decision": "block", + "reason": $reason, + "systemMessage": $msg + }' + exit 0 + fi +fi + # ======================================== # Check BitLesson Delta Section (all non-finalize rounds) # ======================================== @@ -742,7 +784,7 @@ GOAL_TRACKER_FILE="$LOOP_DIR/goal-tracker.md" # Skip this check in Finalize Phase, Review Phase, or when review_started is already true (skip-impl mode) # - Finalize Phase: goal tracker was already initialized before COMPLETE -# - Review Phase (review_started=true): skip-impl mode skips implementation, no goal tracker needed +# - Review Phase: later rounds may update only the mutable section, so Round 0 placeholder checks no longer apply if [[ "$IS_FINALIZE_PHASE" != "true" ]] && [[ "$REVIEW_STARTED" != "true" ]] && [[ "$CURRENT_ROUND" -eq 0 ]] && [[ -f "$GOAL_TRACKER_FILE" ]]; then # Check if goal-tracker.md still contains placeholder text # Extract each section and check for generic placeholder pattern within that section @@ -1235,6 +1277,79 @@ Follow the plan's per-task routing tags strictly: ROUTING_EOF } +# Stop the loop when mainline progress has stalled for too many consecutive rounds. +# Arguments: $1=stall_count, $2=last_verdict +stop_for_mainline_drift() { + local stall_count="$1" + local last_verdict="$2" + + upsert_state_fields "$STATE_FILE" \ + "${FIELD_MAINLINE_STALL_COUNT}=${stall_count}" \ + "${FIELD_LAST_MAINLINE_VERDICT}=${last_verdict}" \ + "${FIELD_DRIFT_STATUS}=${DRIFT_STATUS_REPLAN_REQUIRED}" + + local fallback="# Mainline Drift Circuit Breaker + +The RLCR loop has been stopped because the mainline failed to advance for {{STALL_COUNT}} consecutive implementation rounds. + +- Last mainline verdict: {{LAST_VERDICT}} +- Drift status: replan_required + +This loop should not continue automatically. Revisit the original plan, recover the round contract, and restart with a narrower mainline objective." + local reason + reason=$(load_and_render_safe "$TEMPLATE_DIR" "block/mainline-drift-stop.md" "$fallback" \ + "STALL_COUNT=$stall_count" \ + "LAST_VERDICT=$last_verdict" \ + "PLAN_FILE=$PLAN_FILE") + + end_loop "$LOOP_DIR" "$STATE_FILE" "$EXIT_STOP" + + jq -n \ + --arg reason "$reason" \ + --arg msg "Loop: Stopped - mainline drift circuit breaker triggered" \ + '{ + "decision": "block", + "reason": $reason, + "systemMessage": $msg + }' + exit 0 +} + +# Block exit when implementation review output omits the required mainline verdict. +# Arguments: $1=review_result_file, $2=review_prompt_file +block_missing_mainline_verdict() { + local review_result_file="$1" + local review_prompt_file="$2" + + local fallback="# Mainline Verdict Missing + +The implementation review output is missing the required line: + +\`Mainline Progress Verdict: ADVANCED / STALLED / REGRESSED\` + +Humanize cannot safely update drift state or choose the correct next-round prompt without this verdict. + +Retry the exit so Codex reruns the implementation review. + +Files: +- Review result: {{REVIEW_RESULT_FILE}} +- Review prompt: {{REVIEW_PROMPT_FILE}}" + local reason + reason=$(load_and_render_safe "$TEMPLATE_DIR" "block/mainline-verdict-missing.md" "$fallback" \ + "REVIEW_RESULT_FILE=$review_result_file" \ + "REVIEW_PROMPT_FILE=$review_prompt_file") + + jq -n \ + --arg reason "$reason" \ + --arg msg "Loop: Blocked - implementation review missing Mainline Progress Verdict" \ + '{ + "decision": "block", + "reason": $reason, + "systemMessage": $msg + }' + exit 0 +} + # Continue review loop when issues are found # Arguments: $1=round_number, $2=review_content continue_review_loop_with_issues() { @@ -1273,6 +1388,7 @@ continue_review_loop_with_issues() { - Notes: [what changed and why] EOF fi + local next_contract_file="$LOOP_DIR/round-${round}-contract.md" local fallback="# Code Review Findings @@ -1284,14 +1400,35 @@ You are in the **Review Phase** of the RLCR loop. Codex has performed a code rev ## Instructions -1. Address all issues marked with [P0-9] severity markers -2. Focus on fixes only - do not add new features -3. Commit your changes after fixing the issues -4. Write your summary to: {{SUMMARY_FILE}}" +1. Re-anchor on the original plan and current goal tracker before changing code +2. Refresh the round contract at {{ROUND_CONTRACT_FILE}} +3. Address only the issues that are truly blocking the current mainline objective or code-review acceptance +4. Record non-blocking follow-up items as queued, not as the main goal +5. Commit your changes after fixing the issues +6. Write your summary to: {{SUMMARY_FILE}}" load_and_render_safe "$TEMPLATE_DIR" "claude/review-phase-prompt.md" "$fallback" \ "REVIEW_CONTENT=$review_content" \ - "SUMMARY_FILE=$next_summary_file" > "$next_prompt_file" + "SUMMARY_FILE=$next_summary_file" \ + "BITLESSON_FILE=$BITLESSON_FILE" \ + "PLAN_FILE=$PLAN_FILE" \ + "GOAL_TRACKER_FILE=$GOAL_TRACKER_FILE" \ + "ROUND_CONTRACT_FILE=$next_contract_file" \ + "CURRENT_ROUND=$round" > "$next_prompt_file" + if [[ "$BITLESSON_REQUIRED" == "true" ]] && ! grep -q 'bitlesson-selector' "$next_prompt_file"; then + cat >> "$next_prompt_file" << EOF + +## BitLesson Selection (REQUIRED FOR EACH FIX TASK) + +Before implementing each fix task, you MUST: + +1. Read @$BITLESSON_FILE +2. Run \`bitlesson-selector\` for each fix task/sub-task to select relevant lesson IDs +3. Follow the selected lesson IDs (or \`NONE\`) during implementation + +Reference: @$BITLESSON_FILE +EOF + fi append_task_tag_routing_note "$next_prompt_file" jq -n \ @@ -1536,6 +1673,53 @@ REVIEW_CONTENT=$(cat "$REVIEW_RESULT_FILE") LAST_LINE=$(echo "$REVIEW_CONTENT" | grep -v '^[[:space:]]*$' | tail -1) LAST_LINE_TRIMMED=$(echo "$LAST_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') +NEXT_MAINLINE_STALL_COUNT="$MAINLINE_STALL_COUNT" +NEXT_LAST_MAINLINE_VERDICT="$LAST_MAINLINE_VERDICT" +NEXT_DRIFT_STATUS="$DRIFT_STATUS" +DRIFT_REPLAN_REQUIRED=false +MAINLINE_DRIFT_STOP=false + +if [[ "$REVIEW_STARTED" != "true" ]]; then + EXTRACTED_MAINLINE_VERDICT=$(extract_mainline_progress_verdict "$REVIEW_CONTENT") + + if [[ "$LAST_LINE_TRIMMED" != "$MARKER_STOP" ]] && [[ "$EXTRACTED_MAINLINE_VERDICT" == "$MAINLINE_VERDICT_UNKNOWN" ]]; then + echo "Implementation review output is missing Mainline Progress Verdict. Blocking exit for safety." >&2 + block_missing_mainline_verdict "$REVIEW_RESULT_FILE" "$REVIEW_PROMPT_FILE" + fi + + case "$EXTRACTED_MAINLINE_VERDICT" in + "$MAINLINE_VERDICT_ADVANCED") + NEXT_MAINLINE_STALL_COUNT=0 + NEXT_LAST_MAINLINE_VERDICT="$MAINLINE_VERDICT_ADVANCED" + NEXT_DRIFT_STATUS="$DRIFT_STATUS_NORMAL" + ;; + "$MAINLINE_VERDICT_STALLED"|"$MAINLINE_VERDICT_REGRESSED") + NEXT_MAINLINE_STALL_COUNT=$((MAINLINE_STALL_COUNT + 1)) + NEXT_LAST_MAINLINE_VERDICT="$EXTRACTED_MAINLINE_VERDICT" + if [[ "$NEXT_MAINLINE_STALL_COUNT" -ge 2 ]]; then + NEXT_DRIFT_STATUS="$DRIFT_STATUS_REPLAN_REQUIRED" + DRIFT_REPLAN_REQUIRED=true + else + NEXT_DRIFT_STATUS="$DRIFT_STATUS_NORMAL" + fi + if [[ "$NEXT_MAINLINE_STALL_COUNT" -ge 3 ]]; then + MAINLINE_DRIFT_STOP=true + fi + ;; + *) + : + ;; + esac + + if [[ "$LAST_LINE_TRIMMED" == "$MARKER_COMPLETE" ]]; then + NEXT_MAINLINE_STALL_COUNT=0 + NEXT_LAST_MAINLINE_VERDICT="$MAINLINE_VERDICT_ADVANCED" + NEXT_DRIFT_STATUS="$DRIFT_STATUS_NORMAL" + DRIFT_REPLAN_REQUIRED=false + MAINLINE_DRIFT_STOP=false + fi +fi + # Handle COMPLETE - enter Review Phase or Finalize Phase if [[ "$LAST_LINE_TRIMMED" == "$MARKER_COMPLETE" ]]; then # In review phase, COMPLETE signal is ignored - only absence of [P0-9] triggers finalize @@ -1563,10 +1747,12 @@ if [[ "$LAST_LINE_TRIMMED" == "$MARKER_COMPLETE" ]]; then else echo "Implementation complete. Entering Review Phase..." >&2 - # Update state to indicate review phase has started - TEMP_FILE="${STATE_FILE}.tmp.$$" - sed "s/^review_started: .*/review_started: true/" "$STATE_FILE" > "$TEMP_FILE" - mv "$TEMP_FILE" "$STATE_FILE" + # Update state to indicate review phase has started and clear drift counters. + upsert_state_fields "$STATE_FILE" \ + "${FIELD_REVIEW_STARTED}=true" \ + "${FIELD_MAINLINE_STALL_COUNT}=0" \ + "${FIELD_LAST_MAINLINE_VERDICT}=${MAINLINE_VERDICT_ADVANCED}" \ + "${FIELD_DRIFT_STATUS}=${DRIFT_STATUS_NORMAL}" REVIEW_STARTED="true" # Create marker file to validate review phase was properly entered @@ -1614,6 +1800,11 @@ Use \`/humanize:cancel-rlcr-loop\` to end this loop." run_and_handle_code_review "$((CURRENT_ROUND + 1))" "Loop: Finalize Phase - Code review passed" fi +if [[ "$MAINLINE_DRIFT_STOP" == "true" ]] && [[ "$LAST_LINE_TRIMMED" != "$MARKER_STOP" ]] && [[ "$LAST_LINE_TRIMMED" != "$MARKER_COMPLETE" ]]; then + echo "Mainline progress stalled for $NEXT_MAINLINE_STALL_COUNT consecutive rounds. Triggering drift circuit breaker." >&2 + stop_for_mainline_drift "$NEXT_MAINLINE_STALL_COUNT" "$NEXT_LAST_MAINLINE_VERDICT" +fi + # Handle STOP - circuit breaker triggered if [[ "$LAST_LINE_TRIMMED" == "$MARKER_STOP" ]]; then echo "" >&2 @@ -1649,9 +1840,11 @@ fi # ======================================== # Update state file for next round -TEMP_FILE="${STATE_FILE}.tmp.$$" -sed "s/^current_round: .*/current_round: $NEXT_ROUND/" "$STATE_FILE" > "$TEMP_FILE" -mv "$TEMP_FILE" "$STATE_FILE" +upsert_state_fields "$STATE_FILE" \ + "${FIELD_CURRENT_ROUND}=${NEXT_ROUND}" \ + "${FIELD_MAINLINE_STALL_COUNT}=${NEXT_MAINLINE_STALL_COUNT}" \ + "${FIELD_LAST_MAINLINE_VERDICT}=${NEXT_LAST_MAINLINE_VERDICT}" \ + "${FIELD_DRIFT_STATUS}=${NEXT_DRIFT_STATUS}" # Create next round prompt NEXT_PROMPT_FILE="$LOOP_DIR/round-${NEXT_ROUND}-prompt.md" @@ -1678,6 +1871,7 @@ if [[ ! -f "$NEXT_SUMMARY_FILE" ]]; then - Notes: [what changed and why] EOF fi +NEXT_CONTRACT_FILE="$LOOP_DIR/round-${NEXT_ROUND}-contract.md" # Build the next round prompt from templates NEXT_ROUND_FALLBACK="# Next Round Instructions @@ -1692,12 +1886,60 @@ Before executing tasks in this round: ## Codex Review {{REVIEW_CONTENT}} -Reference: {{PLAN_FILE}}, {{GOAL_TRACKER_FILE}}, {{BITLESSON_FILE}}" -load_and_render_safe "$TEMPLATE_DIR" "claude/next-round-prompt.md" "$NEXT_ROUND_FALLBACK" \ - "PLAN_FILE=$PLAN_FILE" \ - "REVIEW_CONTENT=$REVIEW_CONTENT" \ - "GOAL_TRACKER_FILE=$GOAL_TRACKER_FILE" \ - "BITLESSON_FILE=$BITLESSON_FILE" > "$NEXT_PROMPT_FILE" +Reference: {{PLAN_FILE}}, {{GOAL_TRACKER_FILE}}, {{ROUND_CONTRACT_FILE}}, {{BITLESSON_FILE}}" +DRIFT_REPLAN_FALLBACK="# Drift Recovery Required + +The mainline has not advanced for {{STALL_COUNT}} consecutive implementation rounds. + +Last mainline verdict: {{LAST_MAINLINE_VERDICT}} + +Before writing code: +- Re-read @{{PLAN_FILE}} +- Re-read @{{GOAL_TRACKER_FILE}} +- Re-read the recent round summaries and review results +- Rewrite @{{ROUND_CONTRACT_FILE}} with a recovery-focused mainline objective + +Do not spend this round clearing queued work. Recover mainline progress first. + +## Codex Review +{{REVIEW_CONTENT}}" + +if [[ "$DRIFT_REPLAN_REQUIRED" == "true" ]]; then + load_and_render_safe "$TEMPLATE_DIR" "claude/drift-replan-prompt.md" "$DRIFT_REPLAN_FALLBACK" \ + "PLAN_FILE=$PLAN_FILE" \ + "REVIEW_CONTENT=$REVIEW_CONTENT" \ + "GOAL_TRACKER_FILE=$GOAL_TRACKER_FILE" \ + "BITLESSON_FILE=$BITLESSON_FILE" \ + "ROUND_CONTRACT_FILE=$NEXT_CONTRACT_FILE" \ + "CURRENT_ROUND=$NEXT_ROUND" \ + "STALL_COUNT=$NEXT_MAINLINE_STALL_COUNT" \ + "LAST_MAINLINE_VERDICT=$NEXT_LAST_MAINLINE_VERDICT" > "$NEXT_PROMPT_FILE" +else + load_and_render_safe "$TEMPLATE_DIR" "claude/next-round-prompt.md" "$NEXT_ROUND_FALLBACK" \ + "PLAN_FILE=$PLAN_FILE" \ + "REVIEW_CONTENT=$REVIEW_CONTENT" \ + "GOAL_TRACKER_FILE=$GOAL_TRACKER_FILE" \ + "BITLESSON_FILE=$BITLESSON_FILE" \ + "ROUND_CONTRACT_FILE=$NEXT_CONTRACT_FILE" \ + "CURRENT_ROUND=$NEXT_ROUND" \ + "STALL_COUNT=$NEXT_MAINLINE_STALL_COUNT" \ + "LAST_MAINLINE_VERDICT=$NEXT_LAST_MAINLINE_VERDICT" > "$NEXT_PROMPT_FILE" +fi + +if [[ "$DRIFT_REPLAN_REQUIRED" == "true" ]] && [[ "$BITLESSON_REQUIRED" == "true" ]] && ! grep -q 'bitlesson-selector' "$NEXT_PROMPT_FILE"; then + cat >> "$NEXT_PROMPT_FILE" << EOF + +## BitLesson Selection (REQUIRED FOR EACH TASK) + +Before executing each task or sub-task, you MUST: + +1. Read @$BITLESSON_FILE +2. Run \`bitlesson-selector\` for each task/sub-task to select relevant lesson IDs +3. Follow the selected lesson IDs (or \`NONE\`) during implementation + +Reference: @$BITLESSON_FILE +EOF +fi if [[ "$AGENT_TEAMS" == "true" ]]; then ENFORCEMENT_BLOCK="**Delegation Warning**: Do NOT implement code yourself in Agent Teams mode; delegate all coding tasks to team members." @@ -1814,6 +2056,9 @@ fi # Build system message SYSTEM_MSG="Loop: Round $NEXT_ROUND/$MAX_ITERATIONS - Codex found issues to address" +if [[ "$DRIFT_REPLAN_REQUIRED" == "true" ]]; then + SYSTEM_MSG="Loop: Round $NEXT_ROUND/$MAX_ITERATIONS - Mainline drift detected, replan required" +fi # Block exit and send review feedback jq -n \ diff --git a/hooks/loop-edit-validator.sh b/hooks/loop-edit-validator.sh index 76cf9c03..7259dce8 100755 --- a/hooks/loop-edit-validator.sh +++ b/hooks/loop-edit-validator.sh @@ -6,7 +6,8 @@ # - Todos files (should use native Task tools instead) # - Prompt files (read-only, generated by Codex) # - State files (managed by hooks, not Claude) -# - Goal tracker after Round 0 +# - Wrong round number contract files +# - Goal tracker edits outside the active loop or that alter the immutable section # - PR loop state files (.humanize/pr-loop/) # - PR loop read-only files (pr-comment, prompt, codex-prompt, pr-check, pr-feedback) # @@ -101,6 +102,10 @@ fi # Detect if we're in Finalize Phase (finalize-state.md exists) STATE_FILE_TO_PARSE=$(resolve_active_state_file "$ACTIVE_LOOP_DIR") +IS_FINALIZE_PHASE=false +if [[ "$STATE_FILE_TO_PARSE" == *"/finalize-state.md" ]]; then + IS_FINALIZE_PHASE=true +fi # Parse state file using strict validation (fail closed on malformed state) if ! parse_state_file_strict "$STATE_FILE_TO_PARSE" 2>/dev/null; then @@ -124,6 +129,11 @@ if is_state_file_path "$FILE_PATH_LOWER"; then exit 2 fi +if [[ "$IS_FINALIZE_PHASE" == "true" ]] && is_round_file_type "$FILE_PATH_LOWER" "contract"; then + finalize_contract_blocked_message "edit" >&2 + exit 2 +fi + # ======================================== # Block Plan Backup Edits # ======================================== @@ -139,20 +149,52 @@ if [[ "$FILENAME" == "plan.md" ]]; then fi # ======================================== -# Block Goal Tracker After Round 0 +# Validate Goal Tracker Edits # ======================================== -if is_goal_tracker_path "$FILE_PATH_LOWER" && [[ "$CURRENT_ROUND" -gt 0 ]]; then - SUMMARY_FILE="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-summary.md" - goal_tracker_blocked_message "$CURRENT_ROUND" "$SUMMARY_FILE" >&2 - exit 2 +if is_goal_tracker_path "$FILE_PATH_LOWER"; then + GOAL_TRACKER_PATH="$ACTIVE_LOOP_DIR/goal-tracker.md" + NORMALIZED_FILE_PATH=$(_normalize_path "$FILE_PATH") + NORMALIZED_GOAL_TRACKER_PATH=$(_normalize_path "$GOAL_TRACKER_PATH") + + if [[ "$NORMALIZED_FILE_PATH" != "$NORMALIZED_GOAL_TRACKER_PATH" ]]; then + goal_tracker_blocked_message "$CURRENT_ROUND" "$GOAL_TRACKER_PATH" >&2 + exit 2 + fi + + if [[ "$CURRENT_ROUND" -gt 0 ]]; then + if ! echo "$HOOK_INPUT" | jq -e '.tool_input | has("old_string") and has("new_string")' >/dev/null 2>&1; then + echo "Error: Missing required field: tool_input.old_string or tool_input.new_string" >&2 + exit 1 + fi + OLD_STRING=$(echo "$HOOK_INPUT" | jq -r '.tool_input.old_string // ""') + if [[ -z "$OLD_STRING" ]]; then + echo "Error: Missing required field: tool_input.old_string" >&2 + exit 1 + fi + + NEW_STRING=$(echo "$HOOK_INPUT" | jq -r '.tool_input.new_string // ""') + REPLACE_ALL=$(echo "$HOOK_INPUT" | jq -r '.tool_input.replace_all // false') + + if ! UPDATED_CONTENT=$(preview_edit_result "$GOAL_TRACKER_PATH" "$OLD_STRING" "$NEW_STRING" "$REPLACE_ALL" 2>/dev/null); then + goal_tracker_blocked_message "$CURRENT_ROUND" "$GOAL_TRACKER_PATH" >&2 + exit 2 + fi + + if ! goal_tracker_mutable_update_allowed "$GOAL_TRACKER_PATH" "$UPDATED_CONTENT"; then + goal_tracker_blocked_message "$CURRENT_ROUND" "$GOAL_TRACKER_PATH" >&2 + exit 2 + fi + fi + + exit 0 fi # ======================================== -# Validate Summary File Round Number +# Validate Summary/Contract File Round Number # ======================================== -if is_round_file_type "$FILE_PATH_LOWER" "summary"; then +if is_round_file_type "$FILE_PATH_LOWER" "summary" || is_round_file_type "$FILE_PATH_LOWER" "contract"; then # Extract filename from path (portable - works in bash and zsh) CLAUDE_FILENAME=$(echo "$FILE_PATH" | sed -n 's|.*\.humanize/rlcr/[^/]*/\(.*\)$|\1|p') if [[ -z "$CLAUDE_FILENAME" ]]; then @@ -161,9 +203,10 @@ if is_round_file_type "$FILE_PATH_LOWER" "summary"; then if [[ -n "$CLAUDE_FILENAME" ]]; then CLAUDE_ROUND=$(extract_round_number "$CLAUDE_FILENAME") + FILE_TYPE=$([[ "$FILE_PATH_LOWER" == *"-contract.md" ]] && echo "contract" || echo "summary") if [[ -n "$CLAUDE_ROUND" ]] && [[ "$CLAUDE_ROUND" != "$CURRENT_ROUND" ]] && ! is_allowlisted_file "$FILE_PATH" "$ACTIVE_LOOP_DIR"; then - CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-summary.md" + CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-${FILE_TYPE}.md" FALLBACK="# Wrong Round Number You tried to {{ACTION}} round-{{CLAUDE_ROUND}}-{{FILE_TYPE}}.md but current round is **{{CURRENT_ROUND}}**. @@ -172,7 +215,7 @@ Edit: {{CORRECT_PATH}}" load_and_render_safe "$TEMPLATE_DIR" "block/wrong-round-number.md" "$FALLBACK" \ "ACTION=edit" \ "CLAUDE_ROUND=$CLAUDE_ROUND" \ - "FILE_TYPE=summary" \ + "FILE_TYPE=$FILE_TYPE" \ "CURRENT_ROUND=$CURRENT_ROUND" \ "CORRECT_PATH=$CORRECT_PATH" >&2 exit 2 diff --git a/hooks/loop-read-validator.sh b/hooks/loop-read-validator.sh index f0b6f71f..02d15202 100755 --- a/hooks/loop-read-validator.sh +++ b/hooks/loop-read-validator.sh @@ -3,10 +3,11 @@ # PreToolUse Hook: Validate Read access for RLCR loop and PR loop files # # Blocks Claude from reading: -# - Wrong round's prompt/summary files (outdated information) +# - Wrong round's prompt/summary/contract files (outdated information) # - Round files from wrong locations (not in .humanize/rlcr/) # - Round files from old session directories # - Todos files (should use native Task tools instead) +# - goal-tracker.md from old RLCR sessions # # PR loop files (.humanize/pr-loop/) are generally allowed to read # to give Claude access to comments, prompts, and feedback. @@ -66,15 +67,26 @@ if is_round_file_type "$FILE_PATH_LOWER" "todos"; then fi # ======================================== -# Check for Round Files (summary/prompt) +# Check for Restricted RLCR Files # ======================================== -if ! is_round_file_type "$FILE_PATH_LOWER" "summary" && ! is_round_file_type "$FILE_PATH_LOWER" "prompt"; then +IS_GOAL_TRACKER=$(is_goal_tracker_path "$FILE_PATH_LOWER" && echo "true" || echo "false") +IS_ROUND_FILE=$( + if is_round_file_type "$FILE_PATH_LOWER" "summary" || \ + is_round_file_type "$FILE_PATH_LOWER" "prompt" || \ + is_round_file_type "$FILE_PATH_LOWER" "contract"; then + echo "true" + else + echo "false" + fi +) + +IN_HUMANIZE_LOOP_DIR=$(is_in_humanize_loop_dir "$FILE_PATH" && echo "true" || echo "false") +if [[ "$IS_ROUND_FILE" != "true" ]] && ! { [[ "$IS_GOAL_TRACKER" == "true" ]] && [[ "$IN_HUMANIZE_LOOP_DIR" == "true" ]]; }; then exit 0 fi CLAUDE_FILENAME=$(basename "$FILE_PATH") -IN_HUMANIZE_LOOP_DIR=$(is_in_humanize_loop_dir "$FILE_PATH" && echo "true" || echo "false") # ======================================== # Find Active Loop and Current Round @@ -90,6 +102,10 @@ fi # Detect if we're in Finalize Phase (finalize-state.md exists) STATE_FILE_TO_PARSE=$(resolve_active_state_file "$ACTIVE_LOOP_DIR") +IS_FINALIZE_PHASE=false +if [[ "$STATE_FILE_TO_PARSE" == *"/finalize-state.md" ]]; then + IS_FINALIZE_PHASE=true +fi # Parse state file using strict validation (fail closed on malformed state) if ! parse_state_file_strict "$STATE_FILE_TO_PARSE" 2>/dev/null; then @@ -98,6 +114,35 @@ if ! parse_state_file_strict "$STATE_FILE_TO_PARSE" 2>/dev/null; then fi CURRENT_ROUND="$STATE_CURRENT_ROUND" +if [[ "$IS_FINALIZE_PHASE" == "true" ]] && is_round_file_type "$FILE_PATH_LOWER" "contract"; then + finalize_contract_blocked_message "read" >&2 + exit 2 +fi + +# ======================================== +# Validate Goal Tracker Path +# ======================================== + +if [[ "$IS_GOAL_TRACKER" == "true" ]] && [[ "$IN_HUMANIZE_LOOP_DIR" == "true" ]]; then + CORRECT_PATH="$ACTIVE_LOOP_DIR/goal-tracker.md" + NORMALIZED_FILE_PATH=$(_normalize_path "$FILE_PATH") + NORMALIZED_CORRECT_PATH=$(_normalize_path "$CORRECT_PATH") + + if [[ "$NORMALIZED_FILE_PATH" != "$NORMALIZED_CORRECT_PATH" ]]; then + FALLBACK="# Wrong Goal Tracker Path + +Read the active loop goal tracker instead: {{CORRECT_PATH}}" + load_and_render_safe "$TEMPLATE_DIR" "block/wrong-file-location.md" "$FALLBACK" \ + "FILE_PATH=$FILE_PATH" \ + "ACTIVE_LOOP_DIR=$ACTIVE_LOOP_DIR" \ + "CURRENT_ROUND=$CURRENT_ROUND" \ + "CORRECT_PATH=$CORRECT_PATH" >&2 + exit 2 + fi + + exit 0 +fi + # ======================================== # Extract Round Number and File Type # ======================================== @@ -113,6 +158,8 @@ if is_round_file_type "$FILE_PATH_LOWER" "summary"; then FILE_TYPE="summary" elif is_round_file_type "$FILE_PATH_LOWER" "prompt"; then FILE_TYPE="prompt" +elif is_round_file_type "$FILE_PATH_LOWER" "contract"; then + FILE_TYPE="contract" fi # ======================================== diff --git a/hooks/loop-write-validator.sh b/hooks/loop-write-validator.sh index 02090265..9c6bdc4b 100755 --- a/hooks/loop-write-validator.sh +++ b/hooks/loop-write-validator.sh @@ -6,8 +6,9 @@ # - Todos files (should use native Task tools instead) # - Prompt files (read-only, generated by Codex) # - Wrong round number summary files +# - Wrong round number contract files # - Summary files outside .humanize/rlcr/ -# - Goal tracker after Round 0 +# - Goal tracker writes outside the active loop or that alter the immutable section # - PR loop state files (.humanize/pr-loop/) # - PR loop read-only files (pr-comment, prompt, codex-prompt, pr-check, pr-feedback) # @@ -101,19 +102,20 @@ fi # ======================================== IS_SUMMARY_FILE=$(is_round_file_type "$FILE_PATH_LOWER" "summary" && echo "true" || echo "false") +IS_CONTRACT_FILE=$(is_round_file_type "$FILE_PATH_LOWER" "contract" && echo "true" || echo "false") IS_FINALIZE_SUMMARY=$(is_finalize_summary_path "$FILE_PATH_LOWER" && echo "true" || echo "false") IN_HUMANIZE_LOOP_DIR=$(is_in_humanize_loop_dir "$FILE_PATH" && echo "true" || echo "false") -# If not a summary file, not a finalize summary, and not in .humanize/rlcr, allow normally -if [[ "$IS_SUMMARY_FILE" == "false" ]] && [[ "$IS_FINALIZE_SUMMARY" == "false" ]] && [[ "$IN_HUMANIZE_LOOP_DIR" == "false" ]]; then +# If not a summary file, not a contract file, not a finalize summary, and not in .humanize/rlcr, allow normally +if [[ "$IS_SUMMARY_FILE" == "false" ]] && [[ "$IS_CONTRACT_FILE" == "false" ]] && [[ "$IS_FINALIZE_SUMMARY" == "false" ]] && [[ "$IN_HUMANIZE_LOOP_DIR" == "false" ]]; then exit 0 fi # For state.md, finalize-state.md, goal-tracker.md, and plan.md in .humanize/rlcr, we need further validation -# For other files in .humanize/rlcr that aren't summaries, allow them +# For other files in .humanize/rlcr that aren't summaries/contracts, allow them FILENAME=$(basename "$FILE_PATH") IS_PLAN_BACKUP=$([[ "$FILENAME" == "plan.md" ]] && echo "true" || echo "false") -if [[ "$IN_HUMANIZE_LOOP_DIR" == "true" ]] && [[ "$IS_SUMMARY_FILE" == "false" ]] && [[ "$IS_FINALIZE_SUMMARY" == "false" ]]; then +if [[ "$IN_HUMANIZE_LOOP_DIR" == "true" ]] && [[ "$IS_SUMMARY_FILE" == "false" ]] && [[ "$IS_CONTRACT_FILE" == "false" ]] && [[ "$IS_FINALIZE_SUMMARY" == "false" ]]; then if ! is_state_file_path "$FILE_PATH_LOWER" && ! is_finalize_state_file_path "$FILE_PATH_LOWER" && ! is_goal_tracker_path "$FILE_PATH_LOWER" && [[ "$IS_PLAN_BACKUP" != "true" ]]; then exit 0 fi @@ -174,6 +176,12 @@ if [[ "$IS_FINALIZE_SUMMARY" == "true" ]] && [[ "$IN_HUMANIZE_LOOP_DIR" == "true fi fi +# There is no active round contract once the loop has entered Finalize Phase. +if [[ "$IS_FINALIZE_PHASE" == "true" ]] && [[ "$IS_CONTRACT_FILE" == "true" ]]; then + finalize_contract_blocked_message "write to" >&2 + exit 2 +fi + # ======================================== # Block Plan Backup Writes # ======================================== @@ -188,26 +196,54 @@ if [[ "$IS_PLAN_BACKUP" == "true" ]]; then fi # ======================================== -# Block Goal Tracker After Round 0 +# Validate Goal Tracker Writes # ======================================== -if is_goal_tracker_path "$FILE_PATH_LOWER" && [[ "$CURRENT_ROUND" -gt 0 ]]; then - SUMMARY_FILE="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-summary.md" - goal_tracker_blocked_message "$CURRENT_ROUND" "$SUMMARY_FILE" >&2 - exit 2 +if is_goal_tracker_path "$FILE_PATH_LOWER"; then + GOAL_TRACKER_PATH="$ACTIVE_LOOP_DIR/goal-tracker.md" + NORMALIZED_FILE_PATH=$(_normalize_path "$FILE_PATH") + NORMALIZED_GOAL_TRACKER_PATH=$(_normalize_path "$GOAL_TRACKER_PATH") + + if [[ "$NORMALIZED_FILE_PATH" != "$NORMALIZED_GOAL_TRACKER_PATH" ]]; then + goal_tracker_blocked_message "$CURRENT_ROUND" "$GOAL_TRACKER_PATH" >&2 + exit 2 + fi + + if [[ "$CURRENT_ROUND" -gt 0 ]]; then + if ! require_tool_input_field "$HOOK_INPUT" "content"; then + exit 1 + fi + + UPDATED_CONTENT=$(echo "$HOOK_INPUT" | jq -r '.tool_input.content // ""') + if ! goal_tracker_mutable_update_allowed "$GOAL_TRACKER_PATH" "$UPDATED_CONTENT"; then + goal_tracker_blocked_message "$CURRENT_ROUND" "$GOAL_TRACKER_PATH" >&2 + exit 2 + fi + fi + + exit 0 fi # ======================================== -# Block Summary Files Outside .humanize/rlcr +# Block Summary/Contract Files Outside .humanize/rlcr # ======================================== -if [[ "$IS_SUMMARY_FILE" == "true" ]] && [[ "$IN_HUMANIZE_LOOP_DIR" == "false" ]]; then - CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-summary.md" - FALLBACK="# Wrong Summary Location +if [[ "$IS_SUMMARY_FILE" == "true" || "$IS_CONTRACT_FILE" == "true" ]] && [[ "$IN_HUMANIZE_LOOP_DIR" == "false" ]]; then + if [[ "$IS_CONTRACT_FILE" == "true" ]]; then + CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-contract.md" + FALLBACK="# Wrong Round Contract Location + +Write the round contract to the correct path: {{CORRECT_PATH}}" + load_and_render_safe "$TEMPLATE_DIR" "block/wrong-contract-location.md" "$FALLBACK" \ + "CORRECT_PATH=$CORRECT_PATH" >&2 + else + CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-summary.md" + FALLBACK="# Wrong Summary Location Write summary to the correct path: {{CORRECT_PATH}}" - load_and_render_safe "$TEMPLATE_DIR" "block/wrong-summary-location.md" "$FALLBACK" \ - "CORRECT_PATH=$CORRECT_PATH" >&2 + load_and_render_safe "$TEMPLATE_DIR" "block/wrong-summary-location.md" "$FALLBACK" \ + "CORRECT_PATH=$CORRECT_PATH" >&2 + fi exit 2 fi @@ -224,14 +260,15 @@ if [[ -z "$CLAUDE_FILENAME" ]]; then fi # ======================================== -# Validate Round Number (for summary files) +# Validate Round Number (for summary/contract files) # ======================================== -if [[ "$IS_SUMMARY_FILE" == "true" ]]; then +if [[ "$IS_SUMMARY_FILE" == "true" || "$IS_CONTRACT_FILE" == "true" ]]; then CLAUDE_ROUND=$(extract_round_number "$CLAUDE_FILENAME") + FILE_TYPE=$([[ "$IS_CONTRACT_FILE" == "true" ]] && echo "contract" || echo "summary") if [[ -n "$CLAUDE_ROUND" ]] && [[ "$CLAUDE_ROUND" != "$CURRENT_ROUND" ]] && ! is_allowlisted_file "$FILE_PATH" "$ACTIVE_LOOP_DIR"; then - CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-summary.md" + CORRECT_PATH="$ACTIVE_LOOP_DIR/round-${CURRENT_ROUND}-${FILE_TYPE}.md" FALLBACK="# Wrong Round Number You tried to {{ACTION}} round-{{CLAUDE_ROUND}}-{{FILE_TYPE}}.md but current round is **{{CURRENT_ROUND}}**. @@ -240,7 +277,7 @@ Write to: {{CORRECT_PATH}}" load_and_render_safe "$TEMPLATE_DIR" "block/wrong-round-number.md" "$FALLBACK" \ "ACTION=write to" \ "CLAUDE_ROUND=$CLAUDE_ROUND" \ - "FILE_TYPE=summary" \ + "FILE_TYPE=$FILE_TYPE" \ "CURRENT_ROUND=$CURRENT_ROUND" \ "CORRECT_PATH=$CORRECT_PATH" >&2 exit 2 diff --git a/prompt-template/block/finalize-contract-access.md b/prompt-template/block/finalize-contract-access.md new file mode 100644 index 00000000..7b757d9d --- /dev/null +++ b/prompt-template/block/finalize-contract-access.md @@ -0,0 +1,7 @@ +# Finalize Contract Access Blocked + +There is no active `round-N-contract.md` during the Finalize Phase. + +- Do not {{ACTION}} historical round contract files. +- Use `finalize-summary.md` for finalize-only notes. +- Use `goal-tracker.md` if you need the current mainline/backlog state. diff --git a/prompt-template/block/goal-tracker-modification.md b/prompt-template/block/goal-tracker-modification.md index f7f26384..30c77835 100644 --- a/prompt-template/block/goal-tracker-modification.md +++ b/prompt-template/block/goal-tracker-modification.md @@ -1,25 +1,14 @@ -# Goal Tracker Modification Blocked (Round {{CURRENT_ROUND}}) +# Goal Tracker Update Blocked (Round {{CURRENT_ROUND}}) -After Round 0, **only Codex can modify the Goal Tracker**. +After Round 0, you may update only the **MUTABLE SECTION** of the active goal tracker. -You CANNOT directly modify `goal-tracker.md` via Write, Edit, or Bash commands. +Use Write or Edit on: +`{{CORRECT_PATH}}` -## How to Request Changes +## Rules -Include a **"Goal Tracker Update Request"** section in your summary file: -`{{SUMMARY_FILE}}` +- Keep the **IMMUTABLE SECTION** unchanged +- Do not modify `goal-tracker.md` via Bash +- Do not write to an old loop session's tracker -Use this format: -```markdown -## Goal Tracker Update Request - -### Requested Changes: -- [E.g., "Mark Task X as completed with evidence: tests pass"] -- [E.g., "Add to Open Issues: discovered Y needs addressing"] -- [E.g., "Plan Evolution: changed approach from A to B because..."] - -### Justification: -[Explain why these changes are needed and how they serve the Ultimate Goal] -``` - -Codex will review your request and update the Goal Tracker if the changes are justified. +If you need Codex to correct tracker drift that you could not safely resolve yourself, include an optional `Goal Tracker Update Request` in your summary. diff --git a/prompt-template/block/mainline-drift-stop.md b/prompt-template/block/mainline-drift-stop.md new file mode 100644 index 00000000..2b7cc53d --- /dev/null +++ b/prompt-template/block/mainline-drift-stop.md @@ -0,0 +1,14 @@ +# Mainline Drift Circuit Breaker + +The RLCR loop has been stopped because the implementation failed to advance the mainline for **{{STALL_COUNT}} consecutive rounds**. + +- Last mainline verdict: `{{LAST_VERDICT}}` +- Plan anchor: `{{PLAN_FILE}}` +- Drift status: `replan_required` + +This loop should not continue automatically. + +Next action: +1. Re-read the original plan +2. Identify why recent rounds kept stalling or regressing +3. Start a fresh RLCR loop with a narrower recovered mainline objective diff --git a/prompt-template/block/mainline-verdict-missing.md b/prompt-template/block/mainline-verdict-missing.md new file mode 100644 index 00000000..bf822e53 --- /dev/null +++ b/prompt-template/block/mainline-verdict-missing.md @@ -0,0 +1,13 @@ +# Mainline Verdict Missing + +The implementation review output is missing the required line: + +`Mainline Progress Verdict: ADVANCED / STALLED / REGRESSED` + +Humanize cannot safely update the drift state or choose the correct next-round prompt without this verdict. + +Retry the exit so Codex reruns the implementation review. + +Files: +- Review result: {{REVIEW_RESULT_FILE}} +- Review prompt: {{REVIEW_PROMPT_FILE}} diff --git a/prompt-template/block/round-contract-bash-write.md b/prompt-template/block/round-contract-bash-write.md new file mode 100644 index 00000000..bc012ffd --- /dev/null +++ b/prompt-template/block/round-contract-bash-write.md @@ -0,0 +1,7 @@ +# Round Contract Bash Write Blocked + +Do not use Bash commands to modify round contract files. + +Use the `Write` or `Edit` tool instead: + +`{{CORRECT_PATH}}` diff --git a/prompt-template/block/round-contract-missing.md b/prompt-template/block/round-contract-missing.md new file mode 100644 index 00000000..35a822b1 --- /dev/null +++ b/prompt-template/block/round-contract-missing.md @@ -0,0 +1,13 @@ +# Round Contract Missing + +Before you try to exit this round, write the current round contract to: +`{{ROUND_CONTRACT_FILE}}` + +The round contract must restate: +- The single **mainline objective** for this round +- The target ACs +- Which issues are truly **blocking** +- Which issues are **queued** and out of scope +- The concrete success criteria for this round + +Do not continue without a round contract. The loop uses it to prevent goal drift. diff --git a/prompt-template/block/wrong-contract-location.md b/prompt-template/block/wrong-contract-location.md new file mode 100644 index 00000000..04060c5f --- /dev/null +++ b/prompt-template/block/wrong-contract-location.md @@ -0,0 +1,5 @@ +# Wrong Round Contract Location + +Round contract files MUST be in the active loop directory. + +**Correct path**: `{{CORRECT_PATH}}` diff --git a/prompt-template/claude/drift-replan-prompt.md b/prompt-template/claude/drift-replan-prompt.md new file mode 100644 index 00000000..a5970c59 --- /dev/null +++ b/prompt-template/claude/drift-replan-prompt.md @@ -0,0 +1,68 @@ +Your work is not finished. Read and execute the below with ultrathink. + +## Drift Recovery Mode + +Codex judged the recent implementation rounds as failing to advance the mainline. + +- Consecutive stalled/regressed rounds: {{STALL_COUNT}} +- Last mainline verdict: {{LAST_MAINLINE_VERDICT}} + +This round is a **drift recovery round**. Do not continue with normal issue-clearing behavior. + +## Original Implementation Plan + +**IMPORTANT**: Re-anchor on the original plan first: +@{{PLAN_FILE}} + +## Required Recovery Re-anchor + +Before changing code: +- Re-read @{{PLAN_FILE}} +- Re-read @{{GOAL_TRACKER_FILE}} +- Re-read the recent round summaries and review results that led here +- Rewrite the round contract at @{{ROUND_CONTRACT_FILE}} + +Your recovery contract must contain: +- Exactly one recovered **mainline objective** +- The 1-2 target ACs that prove mainline progress this round +- The root cause of recent drift or stagnation +- Which issues are truly **blocking** the recovered mainline objective +- Which issues remain **queued** and explicitly out of scope +- Concrete success criteria that would change the verdict back to `ADVANCED` + +Do not start implementation until the recovery contract exists. + +## Task Lane Rules + +Use the Task system (TaskCreate, TaskUpdate, TaskList) with one required tag per task: +- `[mainline]` for plan-derived work that directly advances the recovered objective +- `[blocking]` for issues that prevent the recovered mainline objective from succeeding safely +- `[queued]` for non-blocking bugs, cleanup, or follow-up work + +Rules: +- This round must prove mainline movement, not just reduce noise +- `[blocking]` work is allowed only when it directly unblocks the recovered mainline objective +- `[queued]` work must stay documented but must NOT replace the recovered objective +- If a new issue does not block the recovered objective, tag it `[queued]` and keep moving on mainline work + +--- +Below is Codex's review result: + +{{REVIEW_CONTENT}} + +--- + +## Goal Tracker Reference + +Before starting work, **read and update** @{{GOAL_TRACKER_FILE}} as needed: +- Keep the immutable section unchanged +- Record the drift/stagnation cause in the mutable section if it changed planning +- Keep blocking vs queued issue classification accurate +- Ensure the tracker and contract now describe the same recovered mainline objective + +## Recovery Guardrails + +- Do not spend this round mostly on queued cleanup +- Do not broaden scope to compensate for previous stalls +- If the original approach was flawed, log the plan evolution explicitly instead of silently changing direction +- If you cannot produce a credible recovered mainline objective, say so in the summary with concrete blockers diff --git a/prompt-template/claude/finalize-phase-prompt.md b/prompt-template/claude/finalize-phase-prompt.md index 4d1c584b..2ee14176 100644 --- a/prompt-template/claude/finalize-phase-prompt.md +++ b/prompt-template/claude/finalize-phase-prompt.md @@ -40,9 +40,10 @@ The code-simplifier agent should focus on: ## Before Exiting -1. Complete all tasks (mark them as completed using TaskUpdate with status "completed") -2. Commit your changes with a descriptive message -3. Write your finalize summary to: **{{FINALIZE_SUMMARY_FILE}}** +1. Complete all `[mainline]` and `[blocking]` tasks (mark them as completed using TaskUpdate with status "completed") +2. `[queued]` tasks may remain only if they are documented as non-blocking follow-up work +3. Commit your changes with a descriptive message +4. Write your finalize summary to: **{{FINALIZE_SUMMARY_FILE}}** Your summary should include: - What simplifications were made diff --git a/prompt-template/claude/finalize-phase-skipped-prompt.md b/prompt-template/claude/finalize-phase-skipped-prompt.md index 654fabdb..5cb01c3c 100644 --- a/prompt-template/claude/finalize-phase-skipped-prompt.md +++ b/prompt-template/claude/finalize-phase-skipped-prompt.md @@ -39,9 +39,10 @@ These constraints are **non-negotiable**: ## Before Exiting -1. Complete all tasks (mark them as completed using TaskUpdate with status "completed") -2. Commit your changes with a descriptive message -3. Write your finalize summary to: **{{FINALIZE_SUMMARY_FILE}}** +1. Complete all `[mainline]` and `[blocking]` tasks (mark them as completed using TaskUpdate with status "completed") +2. `[queued]` tasks may remain only if they are documented as non-blocking follow-up work +3. Commit your changes with a descriptive message +4. Write your finalize summary to: **{{FINALIZE_SUMMARY_FILE}}** Your summary should include: - What work was done diff --git a/prompt-template/claude/goal-tracker-update-request.md b/prompt-template/claude/goal-tracker-update-request.md index 4c00d483..b685fd51 100644 --- a/prompt-template/claude/goal-tracker-update-request.md +++ b/prompt-template/claude/goal-tracker-update-request.md @@ -1,11 +1,12 @@ -**If Goal Tracker needs updates**, include this section in your summary: +**Optional fallback**: if you could not safely update the mutable section of `goal-tracker.md` directly, include this section in your summary: ```markdown ## Goal Tracker Update Request ### Requested Changes: - [E.g., "Mark Task X as completed with evidence: tests pass"] -- [E.g., "Add to Open Issues: discovered Y needs addressing"] +- [E.g., "Add to Blocking Side Issues: bug Y blocks AC-2"] +- [E.g., "Add to Queued Side Issues: cleanup Z is non-blocking"] - [E.g., "Plan Evolution: changed approach from A to B because..."] - [E.g., "Defer Task Z because... (impact on AC: none/minimal)"] @@ -13,4 +14,4 @@ [Explain why these changes are needed and how they serve the Ultimate Goal] ``` -Codex will review your request and update the Goal Tracker if justified. +Codex will review your request and reconcile the Goal Tracker if justified. diff --git a/prompt-template/claude/next-round-prompt.md b/prompt-template/claude/next-round-prompt.md index b3aaff01..fd1b1cfe 100644 --- a/prompt-template/claude/next-round-prompt.md +++ b/prompt-template/claude/next-round-prompt.md @@ -9,8 +9,35 @@ This plan contains the full scope of work and requirements. Ensure your work ali --- -For all tasks that need to be completed, please use the Task system (TaskCreate, TaskUpdate, TaskList) to track each item in order of importance. -You are strictly prohibited from only addressing the most important issues - you MUST create Tasks for ALL discovered issues and attempt to resolve each one. +## Round Re-anchor (REQUIRED FIRST STEP) + +Before writing code: +- Re-read @{{PLAN_FILE}} +- Re-read @{{GOAL_TRACKER_FILE}} +- Re-read the most recent round summaries/reviews that led to this round +- Write the current round contract to @{{ROUND_CONTRACT_FILE}} + +Your round contract must contain: +- Exactly one **mainline objective** +- The 1-2 target ACs for this round +- Which issues are truly **blocking** that mainline objective +- Which issues are **queued** and explicitly out of scope +- Concrete success criteria for this round + +Do not start implementation until the round contract exists. + +## Task Lane Rules + +Use the Task system (TaskCreate, TaskUpdate, TaskList) with one required tag per task: +- `[mainline]` for plan-derived work that directly advances this round's objective +- `[blocking]` for issues that prevent the mainline objective from succeeding safely +- `[queued]` for non-blocking bugs, cleanup, or follow-up work + +Rules: +- `[mainline]` work is the round's primary success condition +- `[blocking]` work is allowed only when it truly blocks the mainline objective +- `[queued]` work must be documented but must NOT replace the round objective +- If a new bug does not block the current objective, tag it `[queued]` and keep moving on mainline work Before executing each task in this round: 1. Read @{{BITLESSON_FILE}} @@ -24,13 +51,25 @@ Below is Codex's review result: --- -## Goal Tracker Reference (READ-ONLY after Round 0) +## Goal Tracker Reference Before starting work, **read** @{{GOAL_TRACKER_FILE}} to understand: - The Ultimate Goal and Acceptance Criteria you're working toward - Which tasks are Active, Completed, or Deferred +- Which side issues are blocking vs queued - Any Plan Evolution that has occurred -- Open Issues that need attention +- The latest side-issue state that needs attention + +**IMPORTANT**: Keep the mutable section of `goal-tracker.md` up to date during the round. +Do NOT change the immutable section after Round 0. +If you cannot safely reconcile the tracker yourself, include an optional "Goal Tracker Update Request" section in your summary (see below). + +## Mainline Guardrails -**IMPORTANT**: You CANNOT directly modify goal-tracker.md after Round 0. -If you need to update the Goal Tracker, include a "Goal Tracker Update Request" section in your summary (see below). +- Keep the mainline objective from @{{ROUND_CONTRACT_FILE}} stable for this round +- Do not let queued issues take over the round +- If Codex reported several findings, classify them into: + - mainline gaps + - blocking side issues + - queued side issues +- Only mainline gaps and blocking side issues should drive the next code changes diff --git a/prompt-template/claude/post-alignment-action-items.md b/prompt-template/claude/post-alignment-action-items.md index 28611ec0..c78e95d0 100644 --- a/prompt-template/claude/post-alignment-action-items.md +++ b/prompt-template/claude/post-alignment-action-items.md @@ -5,3 +5,4 @@ This round follows a Full Goal Alignment Check. Pay special attention to: - **Forgotten Items**: Codex may have identified tasks that were being ignored. Address them. - **AC Status**: If any Acceptance Criteria were marked NOT MET, prioritize work toward those. - **Deferred Items**: If any deferrals were flagged as unjustified, un-defer them now. +- **Queued Issues**: Keep non-blocking follow-up work queued unless it now clearly blocks mainline progress. diff --git a/prompt-template/claude/review-phase-prompt.md b/prompt-template/claude/review-phase-prompt.md index 158ca0f0..e180e418 100644 --- a/prompt-template/claude/review-phase-prompt.md +++ b/prompt-template/claude/review-phase-prompt.md @@ -2,14 +2,39 @@ You are in the **Review Phase**. Codex has performed a code review and found issues that need to be addressed. +## Required Re-anchor + +Before touching code: +- Re-read the original plan at @{{PLAN_FILE}} +- Re-read the goal tracker at @{{GOAL_TRACKER_FILE}} +- Refresh the current round contract at @{{ROUND_CONTRACT_FILE}} + +The round contract must preserve a single mainline objective. Code review findings do NOT automatically become the new round objective. + ## Review Results {{REVIEW_CONTENT}} +## Issue Classification + +Classify each review finding before acting on it: +- **blocking side issue**: prevents the current mainline objective from succeeding safely or prevents review acceptance +- **queued side issue**: valid follow-up, but does not block the current round objective + +Queued issues may be documented, but they must NOT take over the round. + +## Task Rules + +Every task must use one lane tag: +- `[blocking]` for review findings that must be fixed now +- `[queued]` for non-blocking follow-up work + +Do not create new `[mainline]` tasks in review phase unless the review proves the previous mainline objective was incomplete. + ## Instructions -1. **Read `.humanize/bitlesson.md` and run `bitlesson-selector`** for each fix task before coding -2. **Address all issues** marked with `[P0-9]` severity markers +1. **Refresh the round contract** at `{{ROUND_CONTRACT_FILE}}` +2. **Address blocking issues first** and keep the mainline objective stable 3. **Focus on fixes only** - do not add new features or make unrelated changes 4. **Commit your changes** after fixing the issues 5. **Write your summary** to: `{{SUMMARY_FILE}}` @@ -17,9 +42,13 @@ You are in the **Review Phase**. Codex has performed a code review and found iss ## Summary Template Your summary should include: -- Which issues were fixed -- How each issue was resolved +- The mainline objective for this round +- Which blocking issues were fixed +- Which issues were reclassified as queued follow-up +- How each fixed issue was resolved - Any issues that could not be resolved (with explanation) +- Confirmation that `goal-tracker.md` was updated if the blocking/queued issue lists changed +- A Goal Tracker Update Request only if tracker reconciliation still needs Codex help ## Important Notes diff --git a/prompt-template/codex/full-alignment-review.md b/prompt-template/codex/full-alignment-review.md index d8ced81b..02997dd8 100644 --- a/prompt-template/codex/full-alignment-review.md +++ b/prompt-template/codex/full-alignment-review.md @@ -47,16 +47,32 @@ Estimated remaining rounds: ? Critical blockers: [list if any] ``` -## Part 2: Implementation Review +## Part 2: Mainline Drift Audit (MANDATORY) + +Determine whether the recent rounds are still serving the original plan: +- Is the current round's mainline objective clear and singular? +- Has Claude been advancing mainline ACs, or mostly clearing side issues? +- Which findings are true **blocking side issues** versus merely **queued side issues**? + +Include a short drift summary: +``` +Mainline Progress Verdict: ADVANCED / STALLED / REGRESSED +Blocking Side Issues: N +Queued Side Issues: N +``` + +The `Mainline Progress Verdict` line is mandatory. If you omit it, the Humanize stop hook will block the round and require the review to be rerun. + +## Part 3: Implementation Review - Conduct a deep critical review of the implementation - Verify Claude's claims match reality - Identify any gaps, bugs, or incomplete work - Reference @{{DOCS_PATH}} for design documents -## Part 3: {{GOAL_TRACKER_UPDATE_SECTION}} +## Part 4: {{GOAL_TRACKER_UPDATE_SECTION}} -## Part 4: Progress Stagnation Check (MANDATORY for Full Alignment Rounds) +## Part 5: Progress Stagnation Check (MANDATORY for Full Alignment Rounds) To implement the original plan at @{{PLAN_FILE}}, we have completed **{{COMPLETED_ITERATIONS}} iterations** (Round 0 to Round {{CURRENT_ROUND}}). @@ -83,10 +99,13 @@ The project's `.humanize/rlcr/{{LOOP_TIMESTAMP}}/` directory contains the histor **If development is stagnating**, write **STOP** (as a single word on its own line) as the last line of your review output @{{REVIEW_RESULT_FILE}} instead of COMPLETE. -## Part 5: Output Requirements +## Part 6: Output Requirements - If issues found OR any AC is NOT MET (including deferred ACs), write your findings to @{{REVIEW_RESULT_FILE}} -- Include specific action items for Claude to address +- Include specific action items for Claude to address, classified into: + - Mainline Gaps + - Blocking Side Issues + - Queued Side Issues - **If development is stagnating** (see Part 4), write "STOP" as the last line - **CRITICAL**: Only write "COMPLETE" as the last line if ALL ACs from the original plan are FULLY MET with no deferrals - DEFERRED items are considered INCOMPLETE - do NOT output COMPLETE if any AC is deferred diff --git a/prompt-template/codex/goal-tracker-update-section.md b/prompt-template/codex/goal-tracker-update-section.md index 77cbedca..fb312db8 100644 --- a/prompt-template/codex/goal-tracker-update-section.md +++ b/prompt-template/codex/goal-tracker-update-section.md @@ -1,17 +1,18 @@ ## Goal Tracker Update Requests (YOUR RESPONSIBILITY) -**Important**: Claude cannot directly modify `goal-tracker.md` after Round 0. If Claude's summary contains a "Goal Tracker Update Request" section, YOU must: +Claude should normally keep the **mutable section** of `goal-tracker.md` up to date directly. If Claude's summary contains a "Goal Tracker Update Request" section, or if you detect tracker drift during review, YOU must: -1. **Evaluate the request**: Is the change justified? Does it serve the Ultimate Goal? -2. **If approved**: Update @{{GOAL_TRACKER_FILE}} yourself with the requested changes: +1. **Evaluate the tracker state**: Is the mutable section still aligned with the Ultimate Goal and current AC progress? +2. **If correction is needed**: Update @{{GOAL_TRACKER_FILE}} yourself with the requested changes: - Move tasks between Active/Completed/Deferred sections as appropriate - Add entries to "Plan Evolution Log" with round number and justification - - Add new issues to "Open Issues" if discovered + - Add new issues to "Blocking Side Issues" or "Queued Side Issues" as appropriate - **NEVER modify the IMMUTABLE SECTION** (Ultimate Goal and Acceptance Criteria) -3. **If rejected**: Include in your review why the request was rejected +3. **If you reject a requested tracker change**: Include in your review why it was rejected Common update requests you should handle: - Task completion: Move from "Active Tasks" to "Completed and Verified" -- New issues: Add to "Open Issues" table +- New blocking issues: Add to "Blocking Side Issues" +- New queued issues: Add to "Queued Side Issues" - Plan changes: Add to "Plan Evolution Log" with your assessment - Deferrals: Only allow with strong justification; add to "Explicitly Deferred" diff --git a/prompt-template/codex/regular-review.md b/prompt-template/codex/regular-review.md index 6d0a8671..7db26ea2 100644 --- a/prompt-template/codex/regular-review.md +++ b/prompt-template/codex/regular-review.md @@ -44,11 +44,28 @@ Include a brief Goal Alignment Summary in your review: ACs: X/Y addressed | Forgotten items: N | Unjustified deferrals: N ``` -## Part 3: {{GOAL_TRACKER_UPDATE_SECTION}} +## Part 3: Required Finding Classification -## Part 4: Output Requirements +You MUST classify your findings into these lanes: +- **Mainline Gaps**: plan-derived work or AC progress that is missing, incomplete, or regressing +- **Blocking Side Issues**: bugs or implementation issues that block the current mainline objective from succeeding safely +- **Queued Side Issues**: valid non-blocking follow-up issues that should be documented but must NOT take over the next round + +Also include a one-line verdict: +``` +Mainline Progress Verdict: ADVANCED / STALLED / REGRESSED +``` + +This verdict line is mandatory. If you omit it, the Humanize stop hook will block the round and require the review to be rerun. + +If Claude mostly worked on queued side issues and failed to advance the mainline, say so explicitly. + +## Part 4: {{GOAL_TRACKER_UPDATE_SECTION}} + +## Part 5: Output Requirements - In short, your review comments can include: problems/findings/blockers; claims that don't match reality; implementation plans for deferred work (to be implemented now); implementation plans for unfinished work; goal alignment issues. +- Your output should be structured so Claude can tell which items are mainline gaps, blocking side issues, and queued side issues. - If after your investigation the actual situation does not match what Claude claims to have completed, or there is pending work to be done, output your review comments to @{{REVIEW_RESULT_FILE}}. - **CRITICAL**: Only output "COMPLETE" as the last line if ALL tasks from the original plan are FULLY completed with no deferrals - DEFERRED items are considered INCOMPLETE - do NOT output COMPLETE if any task is deferred diff --git a/scripts/humanize.sh b/scripts/humanize.sh index 1613dd62..a64c18ba 100755 --- a/scripts/humanize.sh +++ b/scripts/humanize.sh @@ -33,6 +33,39 @@ humanize_split_to_array() { fi } +# Parse issue breakdown from goal-tracker.md +# Returns: blocking_issues|queued_issues|open_issues +humanize_parse_goal_tracker_issue_counts() { + local tracker_file="$1" + if [[ ! -f "$tracker_file" ]]; then + echo "0|0|0" + return + fi + + _count_table_data_rows() { + local row_count + row_count=$(sed -n "/$1/,/$2/p" "$tracker_file" | grep -cE '^\|' || true) + row_count=${row_count:-0} + echo $((row_count > 2 ? row_count - 2 : 0)) + } + + local blocking_issues + local queued_issues + local open_issues + + blocking_issues=$(_count_table_data_rows '### Blocking Side Issues' '^###') + queued_issues=$(_count_table_data_rows '### Queued Side Issues' '^###') + open_issues=$((blocking_issues + queued_issues)) + + # Legacy schema only had Open Issues; treat them as blocking for safety. + if [[ "$open_issues" -eq 0 ]]; then + open_issues=$(_count_table_data_rows '### Open Issues' '^###') + blocking_issues="$open_issues" + fi + + echo "${blocking_issues}|${queued_issues}|${open_issues}" +} + # Parse goal-tracker.md and return summary values # Returns: total_acs|completed_acs|active_tasks|completed_tasks|deferred_tasks|open_issues|goal_summary humanize_parse_goal_tracker() { @@ -105,9 +138,10 @@ humanize_parse_goal_tracker() { local deferred_tasks deferred_tasks=$(_count_table_data_rows '### Explicitly Deferred' '^###') - # Count Open Issues - local open_issues - open_issues=$(_count_table_data_rows '### Open Issues' '^###') + # Count Open Issues (new schema prefers Blocking/Queued Side Issues; old schema used Open Issues) + local -a issue_parts + humanize_split_to_array issue_parts "$(humanize_parse_goal_tracker_issue_counts "$tracker_file")" + local open_issues="${issue_parts[2]}" # Extract Ultimate Goal summary (first content line after heading) local goal_summary @@ -364,8 +398,11 @@ _humanize_monitor_codex() { local review_started=$(grep -E "^review_started:" "$state_file" 2>/dev/null | sed 's/review_started: *//' | tr -d ' ') local agent_teams=$(grep -E "^agent_teams:" "$state_file" 2>/dev/null | sed 's/agent_teams: *//' | tr -d ' ') local push_every_round=$(grep -E "^push_every_round:" "$state_file" 2>/dev/null | sed 's/push_every_round: *//' | tr -d ' ') + local mainline_stall_count=$(grep -E "^mainline_stall_count:" "$state_file" 2>/dev/null | sed 's/mainline_stall_count: *//' | tr -d ' ') + local last_mainline_verdict=$(grep -E "^last_mainline_verdict:" "$state_file" 2>/dev/null | sed 's/last_mainline_verdict: *//' | tr -d ' ') + local drift_status=$(grep -E "^drift_status:" "$state_file" 2>/dev/null | sed 's/drift_status: *//' | tr -d ' ') - echo "${current_round:-N/A}|${max_iterations:-N/A}|${full_review_round:-N/A}|${codex_model:-N/A}|${codex_effort:-N/A}|${started_at:-N/A}|${plan_file:-N/A}|${ask_codex_question:-false}|${review_started:-false}|${agent_teams:-}|${push_every_round:-}" + echo "${current_round:-N/A}|${max_iterations:-N/A}|${full_review_round:-N/A}|${codex_model:-N/A}|${codex_effort:-N/A}|${started_at:-N/A}|${plan_file:-N/A}|${ask_codex_question:-false}|${review_started:-false}|${agent_teams:-}|${push_every_round:-}|${mainline_stall_count:-0}|${last_mainline_verdict:-unknown}|${drift_status:-normal}" } # Internal wrappers that call top-level functions @@ -405,6 +442,9 @@ _humanize_monitor_codex() { local review_started="${state_parts[8]:-false}" local agent_teams="${state_parts[9]:-}" local push_every_round="${state_parts[10]:-}" + local mainline_stall_count="${state_parts[11]:-0}" + local last_mainline_verdict="${state_parts[12]:-unknown}" + local drift_status="${state_parts[13]:-normal}" # Parse goal-tracker.md local -a goal_parts @@ -416,6 +456,10 @@ _humanize_monitor_codex() { local deferred_tasks="${goal_parts[4]}" local open_issues="${goal_parts[5]}" local goal_summary="${goal_parts[6]}" + local -a issue_parts + _split_to_array issue_parts "$(humanize_parse_goal_tracker_issue_counts "$goal_tracker_file")" + local blocking_issues="${issue_parts[0]}" + local queued_issues="${issue_parts[1]}" # Parse git status local -a git_parts @@ -548,18 +592,35 @@ _humanize_monitor_codex() { fi team_mode_segment=" | Team Mode: ${team_color}${team_display}${reset}" fi - printf "${magenta}Status:${reset} ${status_line} | Codex Ask Question: ${ask_q_color}${ask_q_display}${reset}${team_mode_segment}${clr_eol}\n" + local drift_segment="" + local drift_color="${dim}" + if [[ "$drift_status" == "replan_required" ]]; then + drift_color="${red}" + elif [[ "${mainline_stall_count:-0}" -gt 0 ]]; then + drift_color="${yellow}" + fi + if [[ -n "$drift_status" ]]; then + drift_segment=" | Drift: ${drift_color}${drift_status}${reset} (${mainline_stall_count}, ${last_mainline_verdict})" + fi + printf "${magenta}Status:${reset} ${status_line} | Codex Ask Question: ${ask_q_color}${ask_q_display}${reset}${team_mode_segment}${drift_segment}${clr_eol}\n" # Progress line (color based on completion status) local ac_color="${green}" [[ "$completed_acs" -lt "$total_acs" ]] && ac_color="${yellow}" - local issue_color="${dim}" - [[ "$open_issues" -gt 0 ]] && issue_color="${red}" + local issue_total_color="${dim}" + [[ "$queued_issues" -gt 0 ]] && issue_total_color="${yellow}" + [[ "$blocking_issues" -gt 0 ]] && issue_total_color="${red}" # Use magenta for Progress and Git labels (status/data lines) printf "${magenta}Progress:${reset} ${ac_color}ACs: ${completed_acs}/${total_acs}${reset} Tasks: ${active_tasks} active, ${completed_tasks} done" [[ "$deferred_tasks" -gt 0 ]] && printf " ${yellow}${deferred_tasks} deferred${reset}" - [[ "$open_issues" -gt 0 ]] && printf " ${issue_color}Issues: ${open_issues}${reset}" + if [[ "$open_issues" -gt 0 ]]; then + printf " ${issue_total_color}Issues: ${open_issues}${reset}" + [[ "$blocking_issues" -gt 0 ]] && printf " (${red}%s blocking${reset}" "$blocking_issues" + [[ "$queued_issues" -gt 0 ]] && printf "%s${yellow}%s queued${reset}" \ + "$([[ "$blocking_issues" -gt 0 ]] && echo ", " || echo "(")" "$queued_issues" + printf ")" + fi printf "${clr_eol}\n" # Git status line (same color as Progress) diff --git a/scripts/lib/monitor-common.sh b/scripts/lib/monitor-common.sh index 26bdaa9b..a6e894ef 100644 --- a/scripts/lib/monitor-common.sh +++ b/scripts/lib/monitor-common.sh @@ -384,6 +384,41 @@ get_pr_loop_phase_display() { # Goal Tracker Parsing # ======================================== +# Parse issue breakdown from goal-tracker.md +# Returns: blocking_issues|queued_issues|open_issues +# Usage: parse_goal_tracker_issue_counts "/path/to/goal-tracker.md" +parse_goal_tracker_issue_counts() { + local tracker_file="$1" + if [[ ! -f "$tracker_file" ]]; then + echo "0|0|0" + return + fi + + _count_table_rows() { + local start_pattern="$1" + local end_pattern="$2" + local row_count + row_count=$(sed -n "/${start_pattern}/,/${end_pattern}/p" "$tracker_file" | grep -cE '^\|' || true) + row_count=${row_count:-0} + echo $((row_count > 2 ? row_count - 2 : 0)) + } + + local blocking_issues + local queued_issues + local open_issues + + blocking_issues=$(_count_table_rows '### Blocking Side Issues' '^###') + queued_issues=$(_count_table_rows '### Queued Side Issues' '^###') + open_issues=$((blocking_issues + queued_issues)) + + if [[ "$open_issues" -eq 0 ]]; then + open_issues=$(_count_table_rows '### Open Issues' '^###') + blocking_issues="$open_issues" + fi + + echo "${blocking_issues}|${queued_issues}|${open_issues}" +} + # Parse goal-tracker.md and return summary values # Returns: total_acs|completed_acs|active_tasks|completed_tasks|deferred_tasks|open_issues|goal_summary # Usage: parse_goal_tracker "/path/to/goal-tracker.md" @@ -448,9 +483,19 @@ parse_goal_tracker() { local deferred_tasks deferred_tasks=$(_count_table_rows '### Explicitly Deferred' '^###') - # Count Open Issues + # Count Open Issues (new schema prefers Blocking/Queued Side Issues; old schema used Open Issues) + local issue_parts_raw local open_issues - open_issues=$(_count_table_rows '### Open Issues' '^###') + issue_parts_raw=$(parse_goal_tracker_issue_counts "$tracker_file") + if [[ -n "${ZSH_VERSION:-}" ]]; then + local -a issue_parts + issue_parts=("${(@s:|:)issue_parts_raw}") + open_issues="${issue_parts[3]}" + else + local -a issue_parts + IFS='|' read -r -a issue_parts <<< "$issue_parts_raw" + open_issues="${issue_parts[2]}" + fi # Extract Ultimate Goal summary local goal_summary diff --git a/scripts/setup-rlcr-loop.sh b/scripts/setup-rlcr-loop.sh index 20c1f32a..c5c079d4 100755 --- a/scripts/setup-rlcr-loop.sh +++ b/scripts/setup-rlcr-loop.sh @@ -48,10 +48,45 @@ BASE_BRANCH="" FULL_REVIEW_ROUND="$DEFAULT_FULL_REVIEW_ROUND" SKIP_IMPL="false" SKIP_IMPL_NO_PLAN="false" +SKIP_IMPL_PLAN_ANCHORED="false" ASK_CODEX_QUESTION="true" AGENT_TEAMS="false" BITLESSON_ALLOW_EMPTY_NONE="true" +extract_plan_goal_content() { + local plan_path="$1" + local goal_section="" + + goal_section=$({ sed -n '/^##[[:space:]]*[Gg]oal\|^##[[:space:]]*[Oo]bjective\|^##[[:space:]]*[Pp]urpose/,/^##/p' "$plan_path" 2>/dev/null || true; } | head -20 | tail -n +2 | head -10) + if [[ -n "$goal_section" ]]; then + printf '%s\n' "$goal_section" + return + fi + + awk ' + /^[[:space:]]*#/ { next } + /^[[:space:]]*$/ { + if (started) { + exit + } + next + } + { + print + started=1 + lines++ + if (lines >= 5) { + exit + } + } + ' "$plan_path" +} + +extract_plan_ac_content() { + local plan_path="$1" + { sed -n '/^##[[:space:]]*[Aa]cceptance\|^##[[:space:]]*[Cc]riteria\|^##[[:space:]]*[Rr]equirements/,/^##/p' "$plan_path" 2>/dev/null || true; } | head -30 | tail -n +2 | head -25 +} + show_help() { cat < "$GOAL_TRACKER_FILE" << 'GOAL_TRACKER_EOF' + if [[ "$SKIP_IMPL_PLAN_ANCHORED" == "true" ]]; then + PLAN_GOAL_CONTENT=$(extract_plan_goal_content "$FULL_PLAN_PATH") + PLAN_AC_CONTENT=$(extract_plan_ac_content "$FULL_PLAN_PATH") + + if [[ -z "$PLAN_GOAL_CONTENT" ]]; then + PLAN_GOAL_CONTENT="Preserve the original plan scope from $PLAN_FILE while resolving code review findings on the current branch." + fi + + if [[ -z "$PLAN_AC_CONTENT" ]]; then + PLAN_AC_CONTENT=$(cat < "$GOAL_TRACKER_FILE" << EOF +# Goal Tracker (Skip Implementation Mode with Plan Anchor) + +This RLCR loop was started with \`--skip-impl\` flag. The implementation phase was skipped, +but an explicit plan was provided and remains the scope anchor for review-only work. + +This tracker is still used to keep the review loop aligned around one mainline objective +and to separate blocking issues from queued follow-up work. + +## IMMUTABLE SECTION + +### Ultimate Goal + +$PLAN_GOAL_CONTENT + +### Acceptance Criteria + +$PLAN_AC_CONTENT + +--- + +## MUTABLE SECTION + +### Plan Version: Review-Only (Updated: Round 0) + +#### Plan Evolution Log +| Round | Change | Reason | Impact on AC | +|-------|--------|--------|--------------| +| 0 | Skip implementation mode initialized around explicit plan anchor | Loop started with \`--skip-impl\` and retained @$PLAN_FILE as scope anchor | Review stays aligned with original plan | + +#### Active Tasks +| Task | Target AC | Status | Notes | +|------|-----------|--------|-------| +| [mainline] Preserve original plan alignment while resolving blocking review findings | Plan ACs in scope | pending | Review-only mode with explicit plan anchor | + +### Blocking Side Issues +| Issue | Discovered Round | Blocking AC | Resolution Path | +|-------|-----------------|-------------|-----------------| + +### Queued Side Issues +| Issue | Discovered Round | Why Not Blocking | Revisit Trigger | +|-------|-----------------|------------------|-----------------| + +### Completed and Verified +| AC | Task | Completed Round | Verified Round | Evidence | +|----|------|-----------------|----------------|----------| + +### Explicitly Deferred +| Task | Original AC | Deferred Since | Justification | When to Reconsider | +|------|-------------|----------------|---------------|-------------------| + +EOF + else + # Create review-only goal tracker for skip-impl mode without a plan (no placeholder text) + cat > "$GOAL_TRACKER_FILE" << 'GOAL_TRACKER_EOF' # Goal Tracker (Skip Implementation Mode) This RLCR loop was started with `--skip-impl` flag. The implementation phase was skipped, and the loop is running in code review mode only. -## Mode: Code Review Only +This tracker is still used to keep the review loop aligned around one mainline objective +and to separate blocking issues from queued follow-up work. + +## IMMUTABLE SECTION + +### Ultimate Goal + +Pass code review for the current branch without regressing existing behavior. + +### Acceptance Criteria + +- AC-1: All blocking `[P0-9]` code review findings are resolved. +- AC-2: Non-blocking follow-up items are explicitly queued and do not block completion. +- AC-3: Finalize phase can complete without introducing new review regressions. + +--- -The goal tracker is not used in skip-impl mode because: -- There is no implementation plan to track -- The loop focuses solely on code review quality -- No acceptance criteria tracking is needed +## MUTABLE SECTION -## What This Loop Does +### Plan Version: Review-Only (Updated: Round 0) -1. Runs `codex review` on changes between base branch and current branch -2. If issues are found, Claude fixes them iteratively -3. When no issues remain, enters finalize phase for code simplification +#### Plan Evolution Log +| Round | Change | Reason | Impact on AC | +|-------|--------|--------|--------------| +| 0 | Skip implementation mode initialized | Loop started with `--skip-impl` | Focus on review-only objective | + +#### Active Tasks +| Task | Target AC | Status | Notes | +|------|-----------|--------|-------| +| [mainline] Pass code review for current branch | AC-1 | pending | Review-only mode | + +### Blocking Side Issues +| Issue | Discovered Round | Blocking AC | Resolution Path | +|-------|-----------------|-------------|-----------------| + +### Queued Side Issues +| Issue | Discovered Round | Why Not Blocking | Revisit Trigger | +|-------|-----------------|------------------|-----------------| + +### Completed and Verified +| AC | Task | Completed Round | Verified Round | Evidence | +|----|------|-----------------|----------------|----------| + +### Explicitly Deferred +| Task | Original AC | Deferred Since | Justification | When to Reconsider | +|------|-------------|----------------|---------------|-------------------| GOAL_TRACKER_EOF + fi else # Normal mode: create full goal tracker @@ -935,11 +1082,8 @@ GOAL_TRACKER_EOF # Extract goal from plan file (look for ## Goal, ## Objective, or first paragraph) # This is a heuristic - Claude will refine it in round 0 # Use ^## without leading whitespace - markdown headers should start at column 0 -GOAL_LINE=$(grep -i -m1 '^##[[:space:]]*\(goal\|objective\|purpose\)' "$FULL_PLAN_PATH" 2>/dev/null || echo "") -if [[ -n "$GOAL_LINE" ]]; then - # Get the content after the heading - # Use || true after sed to ignore SIGPIPE when head closes the pipe early (pipefail mode) - GOAL_SECTION=$({ sed -n '/^##[[:space:]]*[Gg]oal\|^##[[:space:]]*[Oo]bjective\|^##[[:space:]]*[Pp]urpose/,/^##/p' "$FULL_PLAN_PATH" || true; } | head -20 | tail -n +2 | head -10) +GOAL_SECTION=$(extract_plan_goal_content "$FULL_PLAN_PATH") +if [[ -n "$GOAL_SECTION" ]]; then echo "$GOAL_SECTION" >> "$GOAL_TRACKER_FILE" else # Use first non-empty, non-heading paragraph as goal description @@ -959,7 +1103,7 @@ GOAL_TRACKER_EOF # Extract acceptance criteria from plan file (look for ## Acceptance, ## Criteria, ## Requirements) # Use ^## without leading whitespace - markdown headers should start at column 0 # Use || true after sed to ignore SIGPIPE when head closes the pipe early (pipefail mode) -AC_SECTION=$({ sed -n '/^##[[:space:]]*[Aa]cceptance\|^##[[:space:]]*[Cc]riteria\|^##[[:space:]]*[Rr]equirements/,/^##/p' "$FULL_PLAN_PATH" 2>/dev/null || true; } | head -30 | tail -n +2 | head -25) +AC_SECTION=$(extract_plan_ac_content "$FULL_PLAN_PATH") if [[ -n "$AC_SECTION" ]]; then echo "$AC_SECTION" >> "$GOAL_TRACKER_FILE" else @@ -982,10 +1126,20 @@ cat >> "$GOAL_TRACKER_FILE" << 'GOAL_TRACKER_EOF' | 0 | Initial plan | - | - | #### Active Tasks - + | Task | Target AC | Status | Tag | Owner | Notes | |------|-----------|--------|-----|-------|-------| -| [To be populated by Claude based on plan] | - | pending | coding or analyze | claude or codex | - | +| [To be populated by Claude based on plan] | - | pending | coding or analyze | claude or codex | mainline task only | + +### Blocking Side Issues + +| Issue | Discovered Round | Blocking AC | Resolution Path | +|-------|-----------------|-------------|-----------------| + +### Queued Side Issues + +| Issue | Discovered Round | Why Not Blocking | Revisit Trigger | +|-------|-----------------|------------------|-----------------| ### Completed and Verified @@ -997,10 +1151,6 @@ cat >> "$GOAL_TRACKER_FILE" << 'GOAL_TRACKER_EOF' | Task | Original AC | Deferred Since | Justification | When to Reconsider | |------|-------------|----------------|---------------|-------------------| -### Open Issues - -| Issue | Discovered Round | Blocking AC | Resolution Path | -|-------|-----------------|-------------|-----------------| GOAL_TRACKER_EOF fi # End of skip-impl goal tracker handling @@ -1043,6 +1193,7 @@ SUMMARY_TMPL_EOF # ======================================== SUMMARY_PATH="$LOOP_DIR/round-0-summary.md" +ROUND_CONTRACT_PATH="$LOOP_DIR/round-0-contract.md" # Create the round-0 summary template with BitLesson Delta section if [[ "$SKIP_IMPL" != "true" ]]; then @@ -1050,6 +1201,28 @@ if [[ "$SKIP_IMPL" != "true" ]]; then fi if [[ "$SKIP_IMPL" == "true" ]]; then + if [[ "$SKIP_IMPL_PLAN_ANCHORED" == "true" ]]; then + cat > "$ROUND_CONTRACT_PATH" << EOF +# Round 0 Contract + +- Mainline Objective: Keep the current branch aligned with @$PLAN_FILE while resolving only review findings that block clean acceptance. +- Target ACs: The original plan acceptance criteria affected by the current branch changes. +- Blocking Side Issues In Scope: Any \`[P0-9]\` findings or regressions that block review acceptance or violate the original plan scope. +- Queued Side Issues Out of Scope: Non-blocking cleanup, follow-up refactors, or future improvements that do not block review acceptance or plan alignment. +- Success Criteria: Code review passes and the current branch still matches the original plan's intended scope. +EOF + else + cat > "$ROUND_CONTRACT_PATH" << 'ROUND_CONTRACT_EOF' +# Round 0 Contract + +- Mainline Objective: Run code review for the current branch and resolve only findings that block clean acceptance. +- Target ACs: AC-1, AC-2 +- Blocking Side Issues In Scope: Any `[P0-9]` findings from the active review cycle. +- Queued Side Issues Out of Scope: Non-blocking cleanup, follow-up refactors, or future improvements that do not block review acceptance. +- Success Criteria: Code review passes with no blocking findings, and any remaining non-blocking follow-up is explicitly queued. +ROUND_CONTRACT_EOF + fi + # Skip-impl mode: create a prompt for code review only cat > "$LOOP_DIR/round-0-prompt.md" << EOF # Skip Implementation Mode - Code Review Loop @@ -1066,6 +1239,11 @@ The loop will automatically run \`codex review\` on your changes when you try to If issues are found (marked with [P0-9] priority), you'll need to fix them before the loop ends. Do not try to execute anything to trigger the review - just stop and it will run automatically. +Before requesting review, read: +- @$PLAN_FILE +- @$GOAL_TRACKER_FILE +- @$ROUND_CONTRACT_PATH + ## Your Task 1. Review your current work @@ -1074,10 +1252,32 @@ Do not try to execute anything to trigger the review - just stop and it will run 4. Repeat until no issues remain 5. Enter finalize phase for code simplification -## Note +## Review Objective -Since this is skip-impl mode, there is no implementation plan to follow. -The goal tracker is not used - focus on fixing code review issues. +Use the round contract as the current anchor: +- Keep one stable mainline objective and do not let it drift +- Treat review findings as \`[blocking]\` only if they block review acceptance +- Record non-blocking follow-up as \`[queued]\` +- Do not let queued work take over the round + +EOF + if [[ "$SKIP_IMPL_PLAN_ANCHORED" == "true" ]]; then + cat >> "$LOOP_DIR/round-0-prompt.md" << EOF +- Keep review-only work aligned with the original plan at @$PLAN_FILE + +Implementation phase is skipped, but the original plan still defines the intended branch scope. + +EOF + else + cat >> "$LOOP_DIR/round-0-prompt.md" << 'EOF' +There is no explicit implementation plan for this loop, so the review-only contract is the primary anchor. + +EOF + fi + + cat >> "$LOOP_DIR/round-0-prompt.md" << EOF + +Keep @$ROUND_CONTRACT_PATH updated if the blocking/queued split changes materially during review iterations. When you're ready for review, write a brief summary of your changes and try to exit (do not try to execute anything, just stop). @@ -1098,8 +1298,21 @@ Before starting implementation, you MUST initialize the Goal Tracker: 1. Read @$GOAL_TRACKER_FILE 2. If the "Ultimate Goal" section says "[To be extracted...]", extract a clear goal statement from the plan 3. If the "Acceptance Criteria" section says "[To be defined...]", define 3-7 specific, testable criteria -4. Populate the "Active Tasks" table with tasks from the plan, mapping each to an AC and filling Tag/Owner -5. Write the updated goal-tracker.md +4. Populate the "Active Tasks" table with MAINLINE tasks from the plan, mapping each to an AC and filling Tag/Owner +5. Record any already-known side issues in either "Blocking Side Issues" or "Queued Side Issues" +6. Write the updated goal-tracker.md + +## Round Contract Setup (REQUIRED BEFORE CODING) + +Before starting implementation, create @$ROUND_CONTRACT_PATH with: + +1. **One mainline objective** for this round +2. **Target ACs** (1-2 ACs only) +3. **Blocking side issues in scope** for this round +4. **Queued side issues out of scope** for this round +5. **Round success criteria** + +Use this contract to keep the round focused. Do NOT let non-blocking bugs or cleanup work replace the mainline objective. **IMPORTANT**: The IMMUTABLE SECTION can only be modified in Round 0. After this round, it becomes read-only. @@ -1107,8 +1320,18 @@ Before starting implementation, you MUST initialize the Goal Tracker: ## Implementation Plan -For all tasks that need to be completed, please use the Task system (TaskCreate, TaskUpdate, TaskList) to track each item in order of importance. -You are strictly prohibited from only addressing the most important issues - you MUST create Tasks for ALL discovered issues and attempt to resolve each one. +For all tasks that need to be completed, please use the Task system (TaskCreate, TaskUpdate, TaskList). + +Every task MUST start with exactly one lane tag: +- \`[mainline]\` for plan-derived work that directly advances the round objective +- \`[blocking]\` for issues that prevent the mainline objective from succeeding safely +- \`[queued]\` for non-blocking bugs, cleanup, or follow-up work + +Rules: +- \`[mainline]\` tasks are the primary success condition for the round +- \`[blocking]\` tasks may be resolved in the round only if they truly block mainline progress +- \`[queued]\` tasks must NOT become the round objective and do NOT need to be cleared before moving on +- If a new issue is not blocking the current objective, tag it \`[queued]\` and keep moving on the mainline ## Task Tag Routing (MUST FOLLOW) @@ -1177,18 +1400,24 @@ cat >> "$LOOP_DIR/round-0-prompt.md" << EOF Throughout your work, you MUST maintain the Goal Tracker: -1. **Before starting a task**: Mark it as "in_progress" in Active Tasks +1. **Before starting a round**: Re-anchor on the original plan and current round contract +2. **Before starting a task**: Mark the relevant mainline task as "in_progress" in Active Tasks - Confirm Tag/Owner routing is correct before execution -2. **After completing a task**: Move it to "Completed and Verified" with evidence (but mark as "pending verification") -3. **If you discover the plan has errors**: +3. **Active Tasks** are MAINLINE tasks only - side issues do not belong there +4. **Blocking Side Issues** are reserved for issues that truly stop mainline progress +5. **Queued Side Issues** are non-blocking and must not take over the round +6. **After completing a mainline task**: Move it to "Completed and Verified" with evidence (but mark as "pending verification") +7. **If you discover the plan has errors**: - Do NOT silently change direction - Add entry to "Plan Evolution Log" with justification - Explain how the change still serves the Ultimate Goal -4. **If you need to defer a task**: +8. **If you need to defer a task**: - Move it to "Explicitly Deferred" section - Provide strong justification - Explain impact on Acceptance Criteria -5. **If you discover new issues**: Add to "Open Issues" table +9. **If you discover new issues**: + - Add to "Blocking Side Issues" only if mainline progress is blocked + - Otherwise add to "Queued Side Issues" or keep them as \`[queued]\` tasks/backlog --- @@ -1197,8 +1426,9 @@ Note: You MUST NOT try to exit \`start-rlcr-loop\` loop by lying or edit loop st After completing the work, please: 0. If you have access to the \`code-simplifier\` agent, use it to review and optimize the code you just wrote 1. Finalize @$GOAL_TRACKER_FILE (this is Round 0, so you are initializing it - see "Goal Tracker Setup" above) -2. Commit your changes with a descriptive commit message -3. Write your work summary into @$SUMMARY_PATH +2. Write your round contract into @$ROUND_CONTRACT_PATH +3. Commit your changes with a descriptive commit message +4. Write your work summary into @$SUMMARY_PATH EOF # Add push instruction only if push_every_round is true diff --git a/tests/robustness/test-goal-tracker-robustness.sh b/tests/robustness/test-goal-tracker-robustness.sh index fe4c025b..88eda6fd 100755 --- a/tests/robustness/test-goal-tracker-robustness.sh +++ b/tests/robustness/test-goal-tracker-robustness.sh @@ -50,6 +50,16 @@ parse_result() { esac } +parse_issue_result() { + local result="$1" + local field="$2" + case "$field" in + blocking_issues) echo "$result" | cut -d'|' -f1 ;; + queued_issues) echo "$result" | cut -d'|' -f2 ;; + open_issues) echo "$result" | cut -d'|' -f3 ;; + esac +} + # ======================================== # Positive Tests - Valid Goal Tracker # ======================================== @@ -438,6 +448,55 @@ else fail "Deferred tasks count" "2" "$DEFERRED_TASKS" fi +# Test 15b: Distinguish blocking vs queued issues in new schema +echo "" +echo "Test 15b: Distinguish blocking vs queued issues" +cat > "$TEST_DIR/goal-tracker-issue-breakdown.md" << 'EOF' +# Goal Tracker + +### Acceptance Criteria + +- AC-1: Test + +--- + +### Blocking Side Issues + +| Issue | Discovered Round | Blocking AC | Resolution Path | +|-------|-----------------|-------------|-----------------| +| Failing review item | 2 | AC-1 | Fix immediately | + +### Queued Side Issues + +| Issue | Discovered Round | Why Not Blocking | Revisit Trigger | +|-------|-----------------|------------------|-----------------| +| Cleanup follow-up | 2 | Cosmetic only | Next refactor | +| Extra test hardening | 3 | Current AC already met | Regression appears | +EOF + +ISSUE_RESULT=$(humanize_parse_goal_tracker_issue_counts "$TEST_DIR/goal-tracker-issue-breakdown.md") +BLOCKING_ISSUES=$(parse_issue_result "$ISSUE_RESULT" blocking_issues) +QUEUED_ISSUES=$(parse_issue_result "$ISSUE_RESULT" queued_issues) +OPEN_ISSUES=$(parse_issue_result "$ISSUE_RESULT" open_issues) +if [[ "$BLOCKING_ISSUES" == "1" ]] && [[ "$QUEUED_ISSUES" == "2" ]] && [[ "$OPEN_ISSUES" == "3" ]]; then + pass "Separates blocking and queued issues in new schema" +else + fail "Issue breakdown" "1 blocking, 2 queued, 3 total" "$ISSUE_RESULT" +fi + +# Test 15c: Legacy open issues fallback maps to blocking count +echo "" +echo "Test 15c: Legacy open issues fallback maps to blocking count" +ISSUE_RESULT=$(humanize_parse_goal_tracker_issue_counts "$TEST_DIR/goal-tracker-issues.md") +BLOCKING_ISSUES=$(parse_issue_result "$ISSUE_RESULT" blocking_issues) +QUEUED_ISSUES=$(parse_issue_result "$ISSUE_RESULT" queued_issues) +OPEN_ISSUES=$(parse_issue_result "$ISSUE_RESULT" open_issues) +if [[ "$BLOCKING_ISSUES" == "2" ]] && [[ "$QUEUED_ISSUES" == "0" ]] && [[ "$OPEN_ISSUES" == "2" ]]; then + pass "Legacy open issues fallback treated as blocking" +else + fail "Legacy issue fallback" "2 blocking, 0 queued, 2 total" "$ISSUE_RESULT" +fi + # Test 16: File with only headers (no content) echo "" echo "Test 16: File with only section headers" diff --git a/tests/robustness/test-hook-system-robustness.sh b/tests/robustness/test-hook-system-robustness.sh index 8f302bb4..5e8413a6 100755 --- a/tests/robustness/test-hook-system-robustness.sh +++ b/tests/robustness/test-hook-system-robustness.sh @@ -317,8 +317,10 @@ echo "" # Test 12: Bash validator blocks state.md modification attempts echo "Test 12: Bash validator blocks state.md modification" # Create RLCR state for the test -mkdir -p "$TEST_DIR/.humanize/rlcr/2026-01-19_12-00-00" -cat > "$TEST_DIR/.humanize/rlcr/2026-01-19_12-00-00/state.md" << 'EOF' +HOOK_LOOP_DIR="$TEST_DIR/.humanize/rlcr/2026-01-19_12-00-00" +OLD_LOOP_DIR="$TEST_DIR/.humanize/rlcr/2026-01-19_11-00-00" +mkdir -p "$HOOK_LOOP_DIR" +cat > "$HOOK_LOOP_DIR/state.md" << 'EOF' --- current_round: 1 max_iterations: 42 @@ -333,6 +335,54 @@ review_started: false plan_tracked: false --- EOF +cat > "$HOOK_LOOP_DIR/goal-tracker.md" << 'EOF' +# Goal Tracker + +## IMMUTABLE SECTION + +### Ultimate Goal +Keep mainline aligned. + +### Acceptance Criteria +- AC-1: Mainline progress is visible every round. + +--- + +## MUTABLE SECTION + +### Plan Version: 1 (Updated: Round 1) + +#### Active Tasks +| Task | Target AC | Status | Notes | +|------|-----------|--------|-------| +| [mainline] Keep AC-1 moving | AC-1 | pending | - | + +### Blocking Side Issues +| Issue | Discovered Round | Blocking AC | Resolution Path | +|-------|-----------------|-------------|-----------------| + +### Queued Side Issues +| Issue | Discovered Round | Why Not Blocking | Revisit Trigger | +|-------|-----------------|------------------|-----------------| +EOF +mkdir -p "$OLD_LOOP_DIR" +cat > "$OLD_LOOP_DIR/goal-tracker.md" << 'EOF' +# Old Goal Tracker + +## IMMUTABLE SECTION + +### Ultimate Goal +Old session tracker. + +### Acceptance Criteria +- AC-1: Old session only. + +--- + +## MUTABLE SECTION + +### Plan Version: 1 (Updated: Round 0) +EOF # Try to modify state.md - this SHOULD be blocked JSON='{"tool_name":"Bash","tool_input":{"command":"echo hacked >> '"$TEST_DIR"'/.humanize/rlcr/2026-01-19_12-00-00/state.md"}}' set +e @@ -366,9 +416,143 @@ else fail "Goal-tracker.md modification" "exit 2 (blocked)" "exit $EXIT_CODE, result: $RESULT" fi -# Test 12c: Unrelated dangerous commands are allowed through (sandbox handles security) +# Test 12c: Write validator allows mutable goal-tracker updates after round 0 +echo "" +echo "Test 12c: Write validator allows mutable goal-tracker updates after round 0" +cat > "$TEST_DIR/goal-tracker-updated.md" << 'EOF' +# Goal Tracker + +## IMMUTABLE SECTION + +### Ultimate Goal +Keep mainline aligned. + +### Acceptance Criteria +- AC-1: Mainline progress is visible every round. + +--- + +## MUTABLE SECTION + +### Plan Version: 1 (Updated: Round 1) + +#### Active Tasks +| Task | Target AC | Status | Notes | +|------|-----------|--------|-------| +| [mainline] Keep AC-1 moving | AC-1 | in_progress | re-anchored | + +### Blocking Side Issues +| Issue | Discovered Round | Blocking AC | Resolution Path | +|-------|-----------------|-------------|-----------------| +| failing test for AC-1 | 1 | AC-1 | fix before exit | + +### Queued Side Issues +| Issue | Discovered Round | Why Not Blocking | Revisit Trigger | +|-------|-----------------|------------------|-----------------| +EOF +UPDATED_CONTENT=$(jq -Rs . < "$TEST_DIR/goal-tracker-updated.md") +JSON='{"tool_name":"Write","tool_input":{"file_path":"'"$HOOK_LOOP_DIR"'/goal-tracker.md","content":'"$UPDATED_CONTENT"'}}' +set +e +RESULT=$(echo "$JSON" | CLAUDE_PROJECT_DIR="$TEST_DIR" bash "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Write allows mutable goal-tracker updates after round 0" +else + fail "Goal-tracker mutable write" "exit 0" "exit $EXIT_CODE, result: $RESULT" +fi + +# Test 12d: Write validator blocks immutable goal-tracker changes after round 0 +echo "" +echo "Test 12d: Write validator blocks immutable goal-tracker changes after round 0" +cat > "$TEST_DIR/goal-tracker-bad.md" << 'EOF' +# Goal Tracker + +## IMMUTABLE SECTION + +### Ultimate Goal +Change the goal entirely. + +### Acceptance Criteria +- AC-1: Mainline progress is visible every round. + +--- + +## MUTABLE SECTION + +### Plan Version: 1 (Updated: Round 1) +EOF +UPDATED_CONTENT=$(jq -Rs . < "$TEST_DIR/goal-tracker-bad.md") +JSON='{"tool_name":"Write","tool_input":{"file_path":"'"$HOOK_LOOP_DIR"'/goal-tracker.md","content":'"$UPDATED_CONTENT"'}}' +set +e +RESULT=$(echo "$JSON" | CLAUDE_PROJECT_DIR="$TEST_DIR" bash "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]]; then + pass "Write blocks immutable goal-tracker changes after round 0" +else + fail "Goal-tracker immutable write" "exit 2" "exit $EXIT_CODE, result: $RESULT" +fi + +# Test 12e: Edit validator allows mutable goal-tracker edits after round 0 +echo "" +echo "Test 12e: Edit validator allows mutable goal-tracker edits after round 0" +JSON='{"tool_name":"Edit","tool_input":{"file_path":"'"$HOOK_LOOP_DIR"'/goal-tracker.md","old_string":"| [mainline] Keep AC-1 moving | AC-1 | pending | - |","new_string":"| [mainline] Keep AC-1 moving | AC-1 | in_progress | re-anchored |"}}' +set +e +RESULT=$(echo "$JSON" | CLAUDE_PROJECT_DIR="$TEST_DIR" bash "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Edit allows mutable goal-tracker updates after round 0" +else + fail "Goal-tracker mutable edit" "exit 0" "exit $EXIT_CODE, result: $RESULT" +fi + +# Test 12f: Edit validator blocks immutable goal-tracker edits after round 0 +echo "" +echo "Test 12ea: Edit validator allows mutable deletions after round 0" +JSON='{"tool_name":"Edit","tool_input":{"file_path":"'"$HOOK_LOOP_DIR"'/goal-tracker.md","old_string":"| [mainline] Keep AC-1 moving | AC-1 | pending | - |","new_string":""}}' +set +e +RESULT=$(echo "$JSON" | CLAUDE_PROJECT_DIR="$TEST_DIR" bash "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Edit allows mutable goal-tracker deletions after round 0" +else + fail "Goal-tracker mutable delete" "exit 0" "exit $EXIT_CODE, result: $RESULT" +fi + +# Test 12f: Edit validator blocks immutable goal-tracker edits after round 0 +echo "" +echo "Test 12f: Edit validator blocks immutable goal-tracker edits after round 0" +JSON='{"tool_name":"Edit","tool_input":{"file_path":"'"$HOOK_LOOP_DIR"'/goal-tracker.md","old_string":"Keep mainline aligned.","new_string":"Change the goal entirely."}}' +set +e +RESULT=$(echo "$JSON" | CLAUDE_PROJECT_DIR="$TEST_DIR" bash "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]]; then + pass "Edit blocks immutable goal-tracker updates after round 0" +else + fail "Goal-tracker immutable edit" "exit 2" "exit $EXIT_CODE, result: $RESULT" +fi + +# Test 12g: Read validator blocks old-session goal tracker +echo "" +echo "Test 12g: Read validator blocks old-session goal tracker" +JSON='{"tool_name":"Read","tool_input":{"file_path":"'"$OLD_LOOP_DIR"'/goal-tracker.md"}}' +set +e +RESULT=$(echo "$JSON" | CLAUDE_PROJECT_DIR="$TEST_DIR" bash "$PROJECT_ROOT/hooks/loop-read-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]]; then + pass "Read blocks old-session goal-tracker.md" +else + fail "Goal-tracker old-session read" "exit 2" "exit $EXIT_CODE, result: $RESULT" +fi + +# Test 12h: Unrelated dangerous commands are allowed through (sandbox handles security) echo "" -echo "Test 12c: Unrelated dangerous commands allowed through (sandbox responsibility)" +echo "Test 12h: Unrelated dangerous commands allowed through (sandbox responsibility)" JSON='{"tool_name":"Bash","tool_input":{"command":"cat /tmp/test; rm -rf /"}}' set +e RESULT=$(echo "$JSON" | CLAUDE_PROJECT_DIR="$TEST_DIR" bash "$PROJECT_ROOT/hooks/loop-bash-validator.sh" 2>&1) diff --git a/tests/robustness/test-setup-scripts-robustness.sh b/tests/robustness/test-setup-scripts-robustness.sh index 13fe21bc..b4ee9299 100755 --- a/tests/robustness/test-setup-scripts-robustness.sh +++ b/tests/robustness/test-setup-scripts-robustness.sh @@ -1046,6 +1046,32 @@ else fail "--skip-impl goal-tracker" "goal-tracker.md exists" "not found" fi +# Test 44b: --skip-impl creates round-0-contract.md +echo "" +echo "Test 44b: --skip-impl creates round-0-contract.md" +if [[ -n "$LOOP_DIR" ]] && [[ -f "$LOOP_DIR/round-0-contract.md" ]]; then + if grep -qi "Mainline Objective" "$LOOP_DIR/round-0-contract.md"; then + pass "--skip-impl creates round-0-contract.md with mainline objective" + else + fail "--skip-impl round contract content" "Mainline Objective text" "$(cat "$LOOP_DIR/round-0-contract.md")" + fi +else + fail "--skip-impl round contract" "round-0-contract.md exists" "not found" +fi + +# Test 44c: --skip-impl prompt references the round contract +echo "" +echo "Test 44c: --skip-impl prompt references round-0-contract.md" +if [[ -n "$LOOP_DIR" ]] && [[ -f "$LOOP_DIR/round-0-prompt.md" ]]; then + if grep -q "round-0-contract.md" "$LOOP_DIR/round-0-prompt.md"; then + pass "--skip-impl prompt references round-0-contract.md" + else + fail "--skip-impl prompt contract reference" "prompt mentions round-0-contract.md" "$(cat "$LOOP_DIR/round-0-prompt.md")" + fi +else + fail "--skip-impl prompt contract reference" "round-0-prompt.md exists" "not found" +fi + # Test 45: --skip-impl with plan file still works echo "" echo "Test 45: --skip-impl with plan file still works" @@ -1074,6 +1100,44 @@ else fi fi +LOOP_DIR_45=$(find "$TEST_DIR/repo45/.humanize/rlcr" -maxdepth 1 -type d -name "20*" 2>/dev/null | head -1) + +echo "" +echo "Test 45b: --skip-impl with plan file preserves plan goal in goal-tracker" +if [[ -n "$LOOP_DIR_45" ]] && [[ -f "$LOOP_DIR_45/goal-tracker.md" ]]; then + if grep -q "Test the setup script robustness" "$LOOP_DIR_45/goal-tracker.md"; then + pass "--skip-impl with plan preserves plan goal anchor" + else + fail "--skip-impl plan goal anchor" "goal-tracker contains plan goal" "$(cat "$LOOP_DIR_45/goal-tracker.md")" + fi +else + fail "--skip-impl plan goal anchor" "goal-tracker.md exists" "not found" +fi + +echo "" +echo "Test 45c: --skip-impl with plan file prompt references original plan" +if [[ -n "$LOOP_DIR_45" ]] && [[ -f "$LOOP_DIR_45/round-0-prompt.md" ]]; then + if grep -q "@plan.md" "$LOOP_DIR_45/round-0-prompt.md"; then + pass "--skip-impl with plan prompt references original plan" + else + fail "--skip-impl plan prompt anchor" "round-0-prompt references @plan.md" "$(cat "$LOOP_DIR_45/round-0-prompt.md")" + fi +else + fail "--skip-impl plan prompt anchor" "round-0-prompt.md exists" "not found" +fi + +echo "" +echo "Test 45d: --skip-impl with plan file contract references original plan alignment" +if [[ -n "$LOOP_DIR_45" ]] && [[ -f "$LOOP_DIR_45/round-0-contract.md" ]]; then + if grep -qi "aligned with @plan.md" "$LOOP_DIR_45/round-0-contract.md"; then + pass "--skip-impl with plan contract references original plan" + else + fail "--skip-impl plan contract anchor" "round-0-contract references @plan.md" "$(cat "$LOOP_DIR_45/round-0-contract.md")" + fi +else + fail "--skip-impl plan contract anchor" "round-0-contract.md exists" "not found" +fi + # ======================================== # Dependency Check Tests # ======================================== diff --git a/tests/robustness/test-state-file-robustness.sh b/tests/robustness/test-state-file-robustness.sh index ae6d0e7f..83f91824 100755 --- a/tests/robustness/test-state-file-robustness.sh +++ b/tests/robustness/test-state-file-robustness.sh @@ -473,6 +473,55 @@ else fail "Parses state with min full_review_round" "return 0" "returned non-zero" fi +# Test 22: State file with drift-tracking fields +echo "" +echo "Test 22: State file with drift-tracking fields" +cat > "$TEST_DIR/state-drift-fields.md" << 'EOF' +--- +current_round: 4 +max_iterations: 12 +review_started: false +base_branch: main +mainline_stall_count: 2 +last_mainline_verdict: stalled +drift_status: replan_required +--- +EOF + +if parse_state_file "$TEST_DIR/state-drift-fields.md"; then + if [[ "$STATE_MAINLINE_STALL_COUNT" == "2" ]] && [[ "$STATE_LAST_MAINLINE_VERDICT" == "stalled" ]] && [[ "$STATE_DRIFT_STATUS" == "replan_required" ]]; then + pass "Parses drift-tracking fields correctly" + else + fail "Parses drift-tracking fields" "stall=2 verdict=stalled drift=replan_required" \ + "stall=$STATE_MAINLINE_STALL_COUNT verdict=$STATE_LAST_MAINLINE_VERDICT drift=$STATE_DRIFT_STATUS" + fi +else + fail "Parses state with drift-tracking fields" "return 0" "returned non-zero" +fi + +# Test 23: Missing drift-tracking fields use safe defaults +echo "" +echo "Test 23: Missing drift-tracking fields use safe defaults" +cat > "$TEST_DIR/state-no-drift-fields.md" << 'EOF' +--- +current_round: 1 +max_iterations: 8 +review_started: false +base_branch: main +--- +EOF + +if parse_state_file "$TEST_DIR/state-no-drift-fields.md"; then + if [[ "$STATE_MAINLINE_STALL_COUNT" == "0" ]] && [[ "$STATE_LAST_MAINLINE_VERDICT" == "unknown" ]] && [[ "$STATE_DRIFT_STATUS" == "normal" ]]; then + pass "Uses safe defaults for drift-tracking fields" + else + fail "Default drift-tracking fields" "stall=0 verdict=unknown drift=normal" \ + "stall=$STATE_MAINLINE_STALL_COUNT verdict=$STATE_LAST_MAINLINE_VERDICT drift=$STATE_DRIFT_STATUS" + fi +else + fail "Parses state without drift-tracking fields" "return 0" "returned non-zero" +fi + # ======================================== # Summary # ======================================== diff --git a/tests/test-agent-teams.sh b/tests/test-agent-teams.sh index 1c685109..de52bb6c 100755 --- a/tests/test-agent-teams.sh +++ b/tests/test-agent-teams.sh @@ -455,6 +455,9 @@ ask_codex_question: false full_review_round: 5 session_id: agent_teams: $agent_teams +mainline_stall_count: 0 +last_mainline_verdict: unknown +drift_status: normal --- STATE_EOF @@ -483,6 +486,16 @@ GT_EOF Implemented features as requested. SUM_EOF + cat > "$LOOP_DIR/round-${round}-contract.md" << CONTRACT_EOF +# Round $round Contract + +- Mainline Objective: Continue the requested implementation round +- Target ACs: AC-1 +- Blocking Side Issues In Scope: none +- Queued Side Issues Out of Scope: none +- Success Criteria: advance the mainline objective without drift +CONTRACT_EOF + # Set up isolated cache directory export XDG_CACHE_HOME="$TEST_DIR/.cache" mkdir -p "$XDG_CACHE_HOME" @@ -536,6 +549,8 @@ MOCK_EOF setup_stophook_test 3 "true" "false" setup_mock_codex_impl_feedback "## Review Feedback +Mainline Progress Verdict: ADVANCED + Some issues found: - Issue 1: Missing error handling @@ -566,6 +581,46 @@ else fail "impl phase with agent_teams=true: next-round prompt contains agent-teams continuation" "round-4-prompt.md exists" "not found (hook exit=$HOOK_EXIT)" fi +# ======================================== +# Test: Drift recovery prompt still preserves agent-teams continuation +# ======================================== + +setup_stophook_test 3 "true" "false" +perl -0pi -e 's/mainline_stall_count: 0/mainline_stall_count: 1/' "$LOOP_DIR/state.md" +perl -0pi -e 's/last_mainline_verdict: unknown/last_mainline_verdict: stalled/' "$LOOP_DIR/state.md" +setup_mock_codex_impl_feedback "## Review Feedback + +Mainline Progress Verdict: STALLED + +- Mainline gap: AC-1 still has no stable implementation +- Blocking side issue: the team is repeating the same non-advancing fix pattern + +Recover the mainline before trying again. + +CONTINUE" + +HOOK_INPUT='{"stop_hook_active": false, "transcript": [], "session_id": ""}' +set +e +RESULT=$(echo "$HOOK_INPUT" | CLAUDE_PROJECT_DIR="$TEST_DIR" bash "$STOP_HOOK" 2>/dev/null) +HOOK_EXIT=$? +set -e + +NEXT_PROMPT="$LOOP_DIR/round-4-prompt.md" +if [[ -f "$NEXT_PROMPT" ]]; then + if grep -q "Drift Recovery Mode" "$NEXT_PROMPT"; then + pass "drift recovery prompt generated for stalled mainline" + else + fail "drift recovery prompt generated for stalled mainline" "Drift Recovery Mode" "not found" + fi + if grep -qi "Agent Teams" "$NEXT_PROMPT"; then + pass "drift recovery prompt keeps agent-teams continuation" + else + fail "drift recovery prompt keeps agent-teams continuation" "agent-teams text in prompt" "not found" + fi +else + fail "drift recovery prompt keeps agent-teams continuation" "round-4-prompt.md exists" "not found (hook exit=$HOOK_EXIT)" +fi + # ======================================== # Test: Implementation phase with agent_teams=false has no continuation # ======================================== @@ -573,6 +628,8 @@ fi setup_stophook_test 3 "false" "false" setup_mock_codex_impl_feedback "## Review Feedback +Mainline Progress Verdict: ADVANCED + Some issues found: - Issue 1: Missing error handling diff --git a/tests/test-allowlist-validators.sh b/tests/test-allowlist-validators.sh index 6c604965..6c80022f 100755 --- a/tests/test-allowlist-validators.sh +++ b/tests/test-allowlist-validators.sh @@ -4,9 +4,9 @@ # # Tests: # - is_allowlisted_file() function in loop-common.sh -# - Read validator allowlist for todos and summaries -# - Write validator allowlist for todos and summaries -# - Edit validator allowlist for todos and summaries +# - Read validator allowlist for todos, summaries, and contracts +# - Write validator allowlist for todos, summaries, and contracts +# - Edit validator allowlist for todos, summaries, and contracts # - Bash validator allowlist for todos files (path-restricted) # @@ -117,6 +117,14 @@ else fail "round-2-summary.md blocked" "false" "true" fi +# Test 6b: Non-allowlisted file - round-0-contract.md +echo "Test 6b: round-0-contract.md is NOT allowlisted" +if ! is_allowlisted_file "$ACTIVE_LOOP_DIR/round-0-contract.md" "$ACTIVE_LOOP_DIR"; then + pass "round-0-contract.md is NOT allowlisted" +else + fail "round-0-contract.md blocked" "false" "true" +fi + # Test 7: Wrong directory - allowlisted filename but wrong path echo "Test 7: round-1-todos.md in wrong directory is NOT allowlisted" if ! is_allowlisted_file "/other/path/round-1-todos.md" "$ACTIVE_LOOP_DIR"; then @@ -158,6 +166,19 @@ else fail "Write validator round-0-summary.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" fi +# Test 9b: Write validator allows current round contract +echo "Test 9b: Write validator allows round-5-contract.md (current round)" +HOOK_INPUT='{"tool_name": "Write", "tool_input": {"file_path": "'$LOOP_DIR'/round-5-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Write validator allows round-5-contract.md" +else + fail "Write validator round-5-contract.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" +fi + # Test 10: Write validator blocks round-3-todos.md (not in allowlist) echo "Test 10: Write validator blocks round-3-todos.md" HOOK_INPUT='{"tool_name": "Write", "tool_input": {"file_path": "'$LOOP_DIR'/round-3-todos.md"}}' @@ -184,6 +205,19 @@ else fail "Write validator round-2-summary.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" fi +# Test 11b: Write validator blocks stale round contract +echo "Test 11b: Write validator blocks round-3-contract.md" +HOOK_INPUT='{"tool_name": "Write", "tool_input": {"file_path": "'$LOOP_DIR'/round-3-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then + pass "Write validator blocks round-3-contract.md" +else + fail "Write validator round-3-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" +fi + echo "" echo "=== Test: Edit Validator Allowlist ===" echo "" @@ -214,6 +248,32 @@ else fail "Edit validator round-1-summary.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" fi +# Test 13b: Edit validator allows current round contract +echo "Test 13b: Edit validator allows round-5-contract.md (current round)" +HOOK_INPUT='{"tool_name": "Edit", "tool_input": {"file_path": "'$LOOP_DIR'/round-5-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Edit validator allows round-5-contract.md" +else + fail "Edit validator round-5-contract.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 13c: Edit validator blocks stale round contract +echo "Test 13c: Edit validator blocks round-0-contract.md" +HOOK_INPUT='{"tool_name": "Edit", "tool_input": {"file_path": "'$LOOP_DIR'/round-0-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then + pass "Edit validator blocks round-0-contract.md" +else + fail "Edit validator round-0-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" +fi + # Test 14: Edit validator blocks round-4-todos.md echo "Test 14: Edit validator blocks round-4-todos.md" HOOK_INPUT='{"tool_name": "Edit", "tool_input": {"file_path": "'$LOOP_DIR'/round-4-todos.md"}}' @@ -257,6 +317,19 @@ else fail "Read validator round-0-summary.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" fi +# Test 16b: Read validator allows current round contract +echo "Test 16b: Read validator allows round-5-contract.md (current round)" +HOOK_INPUT='{"tool_name": "Read", "tool_input": {"file_path": "'$LOOP_DIR'/round-5-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-read-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Read validator allows round-5-contract.md" +else + fail "Read validator round-5-contract.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" +fi + # Test 17: Read validator blocks round-3-todos.md echo "Test 17: Read validator blocks round-3-todos.md" HOOK_INPUT='{"tool_name": "Read", "tool_input": {"file_path": "'$LOOP_DIR'/round-3-todos.md"}}' @@ -283,6 +356,19 @@ else fail "Read validator round-3-summary.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" fi +# Test 18b: Read validator blocks stale round contract +echo "Test 18b: Read validator blocks round-3-contract.md" +HOOK_INPUT='{"tool_name": "Read", "tool_input": {"file_path": "'$LOOP_DIR'/round-3-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-read-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then + pass "Read validator blocks round-3-contract.md" +else + fail "Read validator round-3-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" +fi + echo "" echo "=== Test: Bash Validator Allowlist (Path-Restricted) ===" echo "" @@ -313,6 +399,19 @@ else fail "Bash validator round-2-todos.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" fi +# Test 20b: Bash validator blocks round-5-contract.md +echo "Test 20b: Bash validator blocks round-5-contract.md" +HOOK_INPUT='{"tool_name": "Bash", "tool_input": {"command": "echo test > '$LOOP_DIR'/round-5-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-bash-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "contract"; then + pass "Bash validator blocks round-5-contract.md" +else + fail "Bash validator round-5-contract.md" "exit 2 with contract error" "exit $EXIT_CODE, output: $RESULT" +fi + # Test 21: Bash validator blocks round-1-todos.md in wrong directory echo "Test 21: Bash validator blocks round-1-todos.md in wrong directory" HOOK_INPUT='{"tool_name": "Bash", "tool_input": {"command": "echo test > /tmp/round-1-todos.md"}}' diff --git a/tests/test-finalize-phase.sh b/tests/test-finalize-phase.sh index 96890a41..483ae665 100755 --- a/tests/test-finalize-phase.sh +++ b/tests/test-finalize-phase.sh @@ -200,6 +200,9 @@ plan_tracked: false start_branch: $current_branch base_branch: main review_started: false +mainline_stall_count: 0 +last_mainline_verdict: unknown +drift_status: normal started_at: 2024-01-01T12:00:00Z --- EOF @@ -223,6 +226,16 @@ Test finalize phase | Task | Target AC | Status | |------|-----------|--------| | Test | AC-1 | completed | +EOF + + cat > "$LOOP_DIR/round-${round}-contract.md" << EOF +# Round $round Contract + +- Mainline Objective: Verify finalize phase coverage +- Target ACs: AC-1 +- Blocking Side Issues In Scope: none +- Queued Side Issues Out of Scope: none +- Success Criteria: current round artifacts are complete EOF } @@ -366,6 +379,18 @@ else fail "Write validator finalize-state.md" "exit 2 with finalize error" "exit $EXIT_CODE, output: $RESULT" fi +echo "T-NEG-5aa: Write validator blocks round contract during Finalize Phase" +HOOK_INPUT='{"tool_name": "Write", "tool_input": {"file_path": "'$LOOP_DIR'/round-5-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "contract"; then + pass "Write validator blocks finalize-phase round contract" +else + fail "Write validator finalize-phase contract" "exit 2 with contract error" "exit $EXIT_CODE, output: $RESULT" +fi + echo "T-NEG-5b: Edit validator blocks finalize-state.md" HOOK_INPUT='{"tool_name": "Edit", "tool_input": {"file_path": "'$LOOP_DIR'/finalize-state.md"}}' set +e @@ -378,6 +403,18 @@ else fail "Edit validator finalize-state.md" "exit 2 with finalize error" "exit $EXIT_CODE, output: $RESULT" fi +echo "T-NEG-5bb: Edit validator blocks round contract during Finalize Phase" +HOOK_INPUT='{"tool_name": "Edit", "tool_input": {"file_path": "'$LOOP_DIR'/round-5-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "contract"; then + pass "Edit validator blocks finalize-phase round contract" +else + fail "Edit validator finalize-phase contract" "exit 2 with contract error" "exit $EXIT_CODE, output: $RESULT" +fi + echo "T-NEG-5c: Bash validator blocks finalize-state.md modification" HOOK_INPUT='{"tool_name": "Bash", "tool_input": {"command": "echo test > '$LOOP_DIR'/finalize-state.md"}}' set +e @@ -513,6 +550,8 @@ setup_test_repo setup_loop_dir 3 10 # current_round: 3, max_iterations: 10 setup_mock_codex "All requirements met. +Mainline Progress Verdict: ADVANCED + COMPLETE" # Create summary for current round @@ -571,6 +610,8 @@ setup_test_repo setup_loop_dir 3 10 # current_round: 3, max_iterations: 10 setup_mock_codex_review_failure "All requirements met. +Mainline Progress Verdict: ADVANCED + COMPLETE" 1 # Create summary for current round @@ -630,6 +671,8 @@ setup_test_repo setup_loop_dir 4 10 # current_round: 4, max_iterations: 10 setup_mock_codex_review_empty_stdout "All requirements met. +Mainline Progress Verdict: ADVANCED + COMPLETE" # Create summary for current round @@ -752,6 +795,8 @@ setup_loop_dir 3 10 # current_round: 3, max_iterations: 10 # Create a mock Codex that outputs review feedback (not COMPLETE) setup_mock_codex "## Review Feedback +Mainline Progress Verdict: ADVANCED + Some issues need to be addressed: - Issue 1: Fix the bug in function X - Issue 2: Add tests for edge case Y @@ -813,6 +858,158 @@ else fail "Review feedback in output" "output contains 'Issue 1' from Codex review" "output does not contain expected feedback" fi +echo "" +echo "=== T-POS-6 / T-NEG-10: Mainline Drift State Machine ===" +echo "" + +# T-POS-6: Two consecutive stalled rounds trigger drift recovery prompt +rm -rf "$TEST_DIR/.humanize" +setup_test_repo +setup_loop_dir 3 10 +perl -0pi -e 's/mainline_stall_count: 0/mainline_stall_count: 1/' "$LOOP_DIR/state.md" +perl -0pi -e 's/last_mainline_verdict: unknown/last_mainline_verdict: stalled/' "$LOOP_DIR/state.md" + +setup_mock_codex "## Review Feedback + +Mainline Progress Verdict: STALLED + +- Mainline gap: AC-1 still lacks a passing implementation path +- Blocking side issue: current approach keeps looping on the same failing path + +Please recover the mainline before trying again. + +CONTINUE" + +cat > "$LOOP_DIR/round-3-summary.md" << 'EOF' +# Round 3 Summary +Tried another implementation pass, but AC-1 is still not advancing. +EOF + +TRANSCRIPT_FILE="$TEST_DIR/transcript.jsonl" +cat > "$TRANSCRIPT_FILE" << 'EOF' +{"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "TodoWrite", "input": {"todos": [{"content": "[mainline] Recover AC-1", "status": "completed", "activeForm": "Recovering AC-1"}]}}]}} +EOF + +echo "T-POS-6: Two stalled rounds trigger drift recovery prompt" +HOOK_INPUT='{"stop_hook_active": false, "transcript_path": "'$TRANSCRIPT_FILE'"}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-codex-stop-hook.sh" 2>&1) +EXIT_CODE=$? +set -e + +if echo "$RESULT" | grep -q '"decision".*block' && [[ -f "$LOOP_DIR/round-4-prompt.md" ]]; then + pass "Drift recovery round blocks exit and creates next prompt" +else + fail "Drift recovery prompt creation" "block with round-4 prompt" "exit $EXIT_CODE, output: $RESULT" +fi + +if grep -q "Drift Recovery Mode" "$LOOP_DIR/round-4-prompt.md"; then + pass "Drift recovery prompt uses special replan template" +else + fail "Drift recovery prompt template" "Drift Recovery Mode in prompt" "$(cat "$LOOP_DIR/round-4-prompt.md")" +fi + +parse_state_file "$LOOP_DIR/state.md" +if [[ "$STATE_CURRENT_ROUND" == "4" ]] && [[ "$STATE_MAINLINE_STALL_COUNT" == "2" ]] && [[ "$STATE_LAST_MAINLINE_VERDICT" == "stalled" ]] && [[ "$STATE_DRIFT_STATUS" == "replan_required" ]]; then + pass "State records drift recovery requirement after second stalled round" +else + fail "Drift recovery state update" "round=4 stall=2 verdict=stalled drift=replan_required" \ + "round=$STATE_CURRENT_ROUND stall=$STATE_MAINLINE_STALL_COUNT verdict=$STATE_LAST_MAINLINE_VERDICT drift=$STATE_DRIFT_STATUS" +fi + +# T-NEG-10a: Missing Mainline Progress Verdict blocks exit and preserves state +rm -rf "$TEST_DIR/.humanize" +setup_test_repo +setup_loop_dir 3 10 +perl -0pi -e 's/mainline_stall_count: 0/mainline_stall_count: 1/' "$LOOP_DIR/state.md" +perl -0pi -e 's/last_mainline_verdict: unknown/last_mainline_verdict: stalled/' "$LOOP_DIR/state.md" + +setup_mock_codex "## Review Feedback + +- Mainline gap: AC-1 still lacks a passing implementation path +- Blocking side issue: current approach keeps looping on the same failing path + +Please restate the mainline more clearly. + +CONTINUE" + +cat > "$LOOP_DIR/round-3-summary.md" << 'EOF' +# Round 3 Summary +Tried another implementation pass, but the review omitted the verdict line. +EOF + +echo "T-NEG-10a: Missing Mainline Progress Verdict blocks exit" +HOOK_INPUT='{"stop_hook_active": false, "transcript_path": "'$TRANSCRIPT_FILE'"}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-codex-stop-hook.sh" 2>&1) +EXIT_CODE=$? +set -e + +if echo "$RESULT" | grep -q '"decision".*block' && echo "$RESULT" | grep -qi "verdict"; then + pass "Missing Mainline Progress Verdict blocks exit" +else + fail "Missing Mainline Progress Verdict" "block with verdict error" "exit $EXIT_CODE, output: $RESULT" +fi + +if [[ ! -f "$LOOP_DIR/round-4-prompt.md" ]]; then + pass "Missing verdict does not generate next-round prompt" +else + fail "Missing verdict prompt generation" "no round-4 prompt" "$(cat "$LOOP_DIR/round-4-prompt.md")" +fi + +parse_state_file "$LOOP_DIR/state.md" +if [[ "$STATE_CURRENT_ROUND" == "3" ]] && [[ "$STATE_MAINLINE_STALL_COUNT" == "1" ]] && [[ "$STATE_LAST_MAINLINE_VERDICT" == "stalled" ]] && [[ "$STATE_DRIFT_STATUS" == "normal" ]]; then + pass "Missing verdict preserves prior drift state" +else + fail "Missing verdict state preservation" "round=3 stall=1 verdict=stalled drift=normal" \ + "round=$STATE_CURRENT_ROUND stall=$STATE_MAINLINE_STALL_COUNT verdict=$STATE_LAST_MAINLINE_VERDICT drift=$STATE_DRIFT_STATUS" +fi + +# T-NEG-10: Third consecutive stalled/regressed round stops the loop +rm -rf "$TEST_DIR/.humanize" +setup_test_repo +setup_loop_dir 3 10 +perl -0pi -e 's/mainline_stall_count: 0/mainline_stall_count: 2/' "$LOOP_DIR/state.md" +perl -0pi -e 's/last_mainline_verdict: unknown/last_mainline_verdict: stalled/' "$LOOP_DIR/state.md" +perl -0pi -e 's/drift_status: normal/drift_status: replan_required/' "$LOOP_DIR/state.md" + +setup_mock_codex "## Review Feedback + +Mainline Progress Verdict: REGRESSED + +- Mainline gap: this round moved farther from AC-1 +- Blocking side issue: recent fixes keep undoing the prior mainline path + +Stop and replan. + +CONTINUE" + +cat > "$LOOP_DIR/round-3-summary.md" << 'EOF' +# Round 3 Summary +The latest attempt regressed the mainline objective again. +EOF + +echo "T-NEG-10: Third stalled/regressed round triggers circuit breaker" +HOOK_INPUT='{"stop_hook_active": false, "transcript_path": "'$TRANSCRIPT_FILE'"}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-codex-stop-hook.sh" 2>&1) +EXIT_CODE=$? +set -e + +if [[ -f "$LOOP_DIR/stop-state.md" ]] && echo "$RESULT" | grep -qi "drift"; then + pass "Third stalled/regressed round stops the loop with drift message" +else + fail "Drift circuit breaker" "stop-state.md and drift message" "exit $EXIT_CODE, files: $(ls "$LOOP_DIR"/*state*.md 2>/dev/null || echo 'none'), output: $RESULT" +fi + +parse_state_file "$LOOP_DIR/stop-state.md" +if [[ "$STATE_MAINLINE_STALL_COUNT" == "3" ]] && [[ "$STATE_LAST_MAINLINE_VERDICT" == "regressed" ]] && [[ "$STATE_DRIFT_STATUS" == "replan_required" ]]; then + pass "Stopped loop preserves final drift state" +else + fail "Preserved drift state on stop" "stall=3 verdict=regressed drift=replan_required" \ + "stall=$STATE_MAINLINE_STALL_COUNT verdict=$STATE_LAST_MAINLINE_VERDICT drift=$STATE_DRIFT_STATUS" +fi + echo "" echo "=== Validator Finalize Phase State Parsing Tests ===" echo "" @@ -850,6 +1047,18 @@ else fail "Read validator finalize-state.md parsing" "exit 0" "exit $EXIT_CODE, output: $RESULT" fi +echo "Test: Read validator blocks round contract during Finalize Phase" +HOOK_INPUT='{"tool_name": "Read", "tool_input": {"file_path": "'$LOOP_DIR'/round-5-contract.md"}}' +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-read-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "contract"; then + pass "Read validator blocks finalize-phase round contract" +else + fail "Read validator finalize-phase contract" "exit 2 with contract error" "exit $EXIT_CODE, output: $RESULT" +fi + echo "Test: Plan-file validator parses finalize-state.md correctly" # The plan-file validator should not error when only finalize-state.md exists HOOK_INPUT='{"prompt": "test prompt"}' diff --git a/tests/test-plan-file-hooks.sh b/tests/test-plan-file-hooks.sh index d2e8af6f..c345d944 100755 --- a/tests/test-plan-file-hooks.sh +++ b/tests/test-plan-file-hooks.sh @@ -60,6 +60,21 @@ setup_mock_codex # Default branch name (set after first git init) DEFAULT_BRANCH="" +create_round_contract() { + local loop_dir="$1" + local round="$2" + + cat > "$loop_dir/round-${round}-contract.md" << EOF +# Round $round Contract + +- Mainline Objective: Keep plan-file integrity checks aligned +- Target ACs: AC-1 +- Blocking Side Issues In Scope: none +- Queued Side Issues Out of Scope: none +- Success Criteria: current round artifacts are present and coherent +EOF +} + setup_test_loop() { cd "$TEST_DIR" @@ -80,6 +95,7 @@ setup_test_loop() { # Create loop directory structure LOOP_DIR="$TEST_DIR/.humanize/rlcr/2024-01-01_12-00-00" + rm -rf "$LOOP_DIR" mkdir -p "$LOOP_DIR" # Create plan file (gitignored) @@ -91,7 +107,12 @@ Test the RLCR loop ## Requirements - Requirement 1 EOF - echo "plans/" >> .gitignore + cat >> .gitignore << 'EOF' +plans/ +.humanize* +.cache/ +bin/ +EOF git add .gitignore git -c commit.gpgsign=false commit -q -m "Add gitignore" @@ -111,6 +132,8 @@ base_branch: $CURRENT_BRANCH review_started: false --- EOF + + create_round_contract "$LOOP_DIR" 0 } echo "=== Test: UserPromptSubmit Hook ===" @@ -466,6 +489,38 @@ else fail "Stop hook YAML parsing" "no YAML parse errors" "output: $RESULT" fi +# Test 8.8b: Stop hook blocks when round contract is missing +echo "Test 8.8b: Stop hook blocks when round contract is missing" +setup_test_loop +rm -f "$LOOP_DIR/round-0-contract.md" +cat > "$LOOP_DIR/round-0-summary.md" << 'EOF' +# Summary +Work done. +EOF +cat > "$LOOP_DIR/goal-tracker.md" << 'EOF' +# Goal Tracker +## IMMUTABLE SECTION +### Ultimate Goal +Test goal +### Acceptance Criteria +- Criterion 1 +## MUTABLE SECTION +### Plan Version: 1 (Updated: Round 0) +#### Active Tasks +| Task | Target AC | Status | Notes | +|------|-----------|--------|-------| +| Task 1 | AC1 | done | - | +EOF +set +e +RESULT=$(echo '{}' | "$PROJECT_ROOT/hooks/loop-codex-stop-hook.sh" 2>&1) +EXIT_CODE=$? +set -e +if echo "$RESULT" | grep -q '"decision"' && echo "$RESULT" | grep -qi "contract"; then + pass "Stop hook blocks when round contract is missing" +else + fail "Stop hook missing round contract" "block with contract error" "exit $EXIT_CODE, output: $RESULT" +fi + # Test 8.9: Hook handles plan_file path with hyphens correctly echo "Test 8.9: Hook handles plan_file with hyphens in path" setup_test_loop @@ -642,6 +697,7 @@ cat > "$TRACKED_LOOP_DIR/round-0-summary.md" << 'EOF' # Summary Work done. EOF +create_round_contract "$TRACKED_LOOP_DIR" 0 cat > "$TRACKED_LOOP_DIR/goal-tracker.md" << 'EOF' # Goal Tracker ## IMMUTABLE SECTION @@ -738,6 +794,7 @@ cat > "$TRACKED_LOOP_DIR/round-0-summary.md" << 'EOF' # Summary Work done. EOF +create_round_contract "$TRACKED_LOOP_DIR" 0 cat > "$TRACKED_LOOP_DIR/goal-tracker.md" << 'EOF' # Goal Tracker ## IMMUTABLE SECTION @@ -822,6 +879,7 @@ cat > "$LOOP_DIR_14_1/round-0-summary.md" << 'EOF' # Summary Work done. EOF +create_round_contract "$LOOP_DIR_14_1" 0 # Goal tracker with ONLY Ultimate Goal placeholder (AC and Tasks are filled) cat > "$LOOP_DIR_14_1/goal-tracker.md" << 'EOF' # Goal Tracker @@ -893,6 +951,7 @@ cat > "$LOOP_DIR_14_2/round-0-summary.md" << 'EOF' # Summary Work done. EOF +create_round_contract "$LOOP_DIR_14_2" 0 # Goal tracker with ONLY AC placeholder (Goal and Tasks are filled) cat > "$LOOP_DIR_14_2/goal-tracker.md" << 'EOF' # Goal Tracker @@ -964,6 +1023,7 @@ cat > "$LOOP_DIR_14_3/round-0-summary.md" << 'EOF' # Summary Work done. EOF +create_round_contract "$LOOP_DIR_14_3" 0 # Goal tracker with ONLY Active Tasks placeholder (Goal and AC are filled) cat > "$LOOP_DIR_14_3/goal-tracker.md" << 'EOF' # Goal Tracker @@ -1033,6 +1093,7 @@ cat > "$LOOP_DIR_14_4/round-0-summary.md" << 'EOF' # Summary Work done. EOF +create_round_contract "$LOOP_DIR_14_4" 0 # Goal tracker with ALL placeholders cat > "$LOOP_DIR_14_4/goal-tracker.md" << 'EOF' # Goal Tracker diff --git a/tests/test-task-tag-routing.sh b/tests/test-task-tag-routing.sh index ae9365f7..3d4bc0fe 100755 --- a/tests/test-task-tag-routing.sh +++ b/tests/test-task-tag-routing.sh @@ -180,6 +180,15 @@ Keep routing behavior stable. | Task | Target AC | Status | Tag | Owner | Notes | |------|-----------|--------|-----|-------|-------| | Keep routing note | AC-1 | in_progress | analyze | codex | - +EOF + cat > "$loop_dir/round-0-contract.md" << 'EOF' +# Round 0 Contract + +- Mainline Objective: Keep routing behavior stable while addressing the current review feedback. +- Target ACs: AC-1 +- Blocking Side Issues In Scope: none +- Queued Side Issues Out of Scope: none +- Success Criteria: Follow-up prompt is generated with routing guidance intact. EOF cat > "$loop_dir/round-0-summary.md" << 'EOF' # Round 0 Summary @@ -197,6 +206,8 @@ setup_test_dir setup_stophook_repo "$TEST_DIR/hook-routing" create_mock_codex "$TEST_DIR/hook-routing/bin" "## Review Feedback +Mainline Progress Verdict: STALLED + Issue remains unresolved. CONTINUE" diff --git a/tests/test-todo-checker.sh b/tests/test-todo-checker.sh index b3e7b072..18b076a2 100755 --- a/tests/test-todo-checker.sh +++ b/tests/test-todo-checker.sh @@ -157,6 +157,36 @@ else fail "In-progress status" "exit 1" "exit $EXIT_CODE" fi +# Test 8b: Queued TodoWrite item does NOT block exit +echo "Test 8b: Queued TodoWrite item" +cat > "$TEST_DIR/transcript-queued.jsonl" << 'EOF' +{"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "TodoWrite", "input": {"todos": [{"content": "[queued] Cleanup follow-up", "status": "pending"}]}}]}} +EOF +set +e +RESULT=$(echo "{\"transcript_path\": \"$TEST_DIR/transcript-queued.jsonl\"}" | python3 "$TODO_CHECKER" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Queued TodoWrite item exits 0" +else + fail "Queued TodoWrite item" "exit 0" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 8c: Lane tags in the middle of TodoWrite content do NOT downgrade blocking tasks +echo "Test 8c: Inline queued tag does not bypass TodoWrite blocker" +cat > "$TEST_DIR/transcript-inline-tag.jsonl" << 'EOF' +{"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "TodoWrite", "input": {"todos": [{"content": "Fix docs mentioning [queued] follow-ups", "status": "pending"}]}}]}} +EOF +set +e +RESULT=$(echo "{\"transcript_path\": \"$TEST_DIR/transcript-inline-tag.jsonl\"}" | python3 "$TODO_CHECKER" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 1 ]] && echo "$RESULT" | grep -q '\[blocking\]'; then + pass "Inline queued tag still blocks TodoWrite item" +else + fail "Inline queued TodoWrite item" "exit 1 with [blocking] output" "exit $EXIT_CODE, output: $RESULT" +fi + # ======================================== # Test Group 3: Transcript Format Variations # ======================================== @@ -357,6 +387,57 @@ else fail "Task with in_progress status" "exit 1" "exit $EXIT_CODE, output: $RESULT" fi +# Test 19b: Queued file-based task does NOT block exit +echo "Test 19b: Queued task does not block" +MOCK_SESSION_19B="session-19b" +mkdir -p "$MOCK_TASKS_BASE/$MOCK_SESSION_19B" +cat > "$MOCK_TASKS_BASE/$MOCK_SESSION_19B/task-1.json" << 'EOF' +{"subject": "[queued] Follow-up cleanup", "status": "pending"} +EOF +set +e +RESULT=$(echo "{\"session_id\": \"$MOCK_SESSION_19B\", \"tasks_base_dir\": \"$MOCK_TASKS_BASE\"}" | python3 "$TODO_CHECKER" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Queued task exits 0" +else + fail "Queued task" "exit 0" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 19c: Explicit blocking tag still blocks +echo "Test 19c: Blocking task still blocks" +MOCK_SESSION_19C="session-19c" +mkdir -p "$MOCK_TASKS_BASE/$MOCK_SESSION_19C" +cat > "$MOCK_TASKS_BASE/$MOCK_SESSION_19C/task-1.json" << 'EOF' +{"subject": "[blocking] Fix failing test", "status": "pending"} +EOF +set +e +RESULT=$(echo "{\"session_id\": \"$MOCK_SESSION_19C\", \"tasks_base_dir\": \"$MOCK_TASKS_BASE\"}" | python3 "$TODO_CHECKER" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 1 ]] && echo "$RESULT" | grep -q '\[blocking\]'; then + pass "Blocking task exits 1 with lane marker" +else + fail "Blocking task" "exit 1 with [blocking] output" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 19d: Inline queued tag in task body does NOT downgrade blocking tasks +echo "Test 19d: Inline queued tag in task body does not bypass blocker" +MOCK_SESSION_19D="session-19d" +mkdir -p "$MOCK_TASKS_BASE/$MOCK_SESSION_19D" +cat > "$MOCK_TASKS_BASE/$MOCK_SESSION_19D/task-1.json" << 'EOF' +{"subject": "Triage review fallout", "description": "Notes mention [queued] cleanup but this task is still active", "status": "pending"} +EOF +set +e +RESULT=$(echo "{\"session_id\": \"$MOCK_SESSION_19D\", \"tasks_base_dir\": \"$MOCK_TASKS_BASE\"}" | python3 "$TODO_CHECKER" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 1 ]] && echo "$RESULT" | grep -q '\[blocking\]'; then + pass "Inline queued tag still blocks file-based task" +else + fail "Inline queued file-based task" "exit 1 with [blocking] output" "exit $EXIT_CODE, output: $RESULT" +fi + # Test 20: Multiple tasks, one incomplete echo "Test 20: Multiple tasks, one incomplete" MOCK_SESSION_20="session-20"