From dceeef7e0f8b70feb516f806903345cf598e564d Mon Sep 17 00:00:00 2001 From: Aidan Reilly Date: Fri, 1 May 2026 11:01:02 +0100 Subject: [PATCH 1/3] fix: scope Stop hook to active workflow only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Stop hook scanned all progress files and blocked Claude from stopping when any stale in_progress workflow existed — even from previous sessions or for different tickets. This caused Claude to hijack sessions and work on tickets the user didn't ask about. Introduce an active-workflow marker (.claude/docs/.active-workflow) that the orchestrator writes when starting or resuming a workflow. The hook now reads only this marker, ignoring all other progress files. Stale markers are auto-cleaned when they point to missing or completed workflows. Co-Authored-By: Claude Opus 4.6 rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- .../skills/docs-orchestrator/SKILL.md | 52 +++++++- .../hooks/workflow-completion-check.sh | 126 ++++++++++-------- 2 files changed, 118 insertions(+), 60 deletions(-) diff --git a/plugins/docs-tools/skills/docs-orchestrator/SKILL.md b/plugins/docs-tools/skills/docs-orchestrator/SKILL.md index 09febae8..da6be344 100644 --- a/plugins/docs-tools/skills/docs-orchestrator/SKILL.md +++ b/plugins/docs-tools/skills/docs-orchestrator/SKILL.md @@ -268,7 +268,7 @@ Every step that produces markdown output also writes a `step-result.json` sideca ## Progress file -Claude writes the progress file directly using the Write tool. Create it after parsing arguments, before step 1. Update it after each step. +Claude writes the progress file directly using the Write tool. Create it after parsing arguments, before step 1. Update it after each step. Also write the active workflow marker at the same time (see [Active workflow marker](#active-workflow-marker)). **Location**: `.claude/docs//workflow/_.json` @@ -322,6 +322,47 @@ The `result` field stores selected sidecar data after each step completes. This A top-level array listing steps in canonical order. This field exists so the Stop hook can determine step ordering without a hardcoded bash array. It **must** always be written by the orchestrator and kept in sync with the YAML step list. +## Active workflow marker + +The active workflow marker tells the Stop hook which workflow (if any) is currently running in this session. Without the marker, the hook allows Claude to stop freely. + +**Location**: `.claude/docs/.active-workflow` + +### When to write the marker + +Write the marker file using the Write tool at the same time as creating or updating the progress file to `"in_progress"` — after parsing arguments, before step 1. If resuming an existing workflow, overwrite any existing marker. + +### Schema + +```json +{ + "ticket": "", + "workflow_type": "", + "progress_file": ".claude/docs//workflow/_.json" +} +``` + +The `progress_file` path must be relative to the project root (matching the path the hook uses to locate the file). + +### When to delete the marker + +Delete `.claude/docs/.active-workflow` when: + +1. The workflow completes — immediately after setting the progress file's `status` to `"completed"` in the [Completion](#completion) section +2. The workflow fails terminally — after setting `status` to `"failed"` (e.g., planning step produces 0 modules and user chooses to stop) + +Do **not** delete the marker between steps. The marker must persist for the entire duration of the workflow so the Stop hook can block premature stops. + +### Overwriting on resume or new workflow + +If the user starts a new workflow (different ticket or different workflow type) or resumes an existing one, overwrite the marker with the new workflow's information. There is only ever one active workflow at a time. The previous marker is implicitly superseded. + +### Edge cases + +- **No marker exists**: The Stop hook allows Claude to stop. This is the correct default for sessions that don't involve a workflow. +- **Marker points to a missing progress file**: The Stop hook cleans up the stale marker and allows stop. +- **Marker exists but workflow status is `"completed"` or `"failed"`**: The Stop hook cleans up the marker and allows stop. + ## Check for existing work Before starting, check for a progress file at `.claude/docs//workflow/_.json`. @@ -335,7 +376,9 @@ Before starting, check for a progress file at `.claude/docs//workflow/`. Resuming from ``." 6. If the user provided additional flags on resume (e.g., `--create-jira`), update the progress file options accordingly -**If no progress file exists**, start from step 1 and create a new progress file. +**If no progress file exists**, start from step 1, create a new progress file, and write the active workflow marker. + +In both cases (new or resume), write the [active workflow marker](#active-workflow-marker) with the current ticket and workflow type. This ensures the Stop hook tracks only this workflow. ## Running workflow steps @@ -397,7 +440,7 @@ After each step completes, apply the rules below. When rules reference sidecar f **planning** - Log: `"Planning completed: N modules"` -- If `module_count` is 0, **warn**: `"Planning produced 0 modules — the plan may be empty. Review plan.md before continuing."` Ask the user whether to proceed or stop. If the user chooses to stop: mark the planning step as `failed` in the progress file, set the workflow status to `"failed"`, log `"Planning stopped by user after 0 modules — workflow cancelled."`, and halt without running subsequent steps +- If `module_count` is 0, **warn**: `"Planning produced 0 modules — the plan may be empty. Review plan.md before continuing."` Ask the user whether to proceed or stop. If the user chooses to stop: mark the planning step as `failed` in the progress file, set the workflow status to `"failed"`, delete the active workflow marker (`.claude/docs/.active-workflow`), log `"Planning stopped by user after 0 modules — workflow cancelled."`, and halt without running subsequent steps **code-evidence** - Log: `"Code evidence retrieved: N topics, N snippets"` @@ -476,7 +519,8 @@ If the user declines, mark the `create-merge-request` step as `skipped` (with `s After all steps complete (or are skipped): 1. Update the progress file: `status → "completed"` -2. Display a summary: +2. Delete the active workflow marker: remove `.claude/docs/.active-workflow` +3. Display a summary: - List all output folders with paths - Note any warnings (tech review didn't reach `HIGH`, planning had 0 modules, code-evidence had 0 snippets, etc.) - Show MR/PR URL from `steps.create-merge-request.result.url` if present diff --git a/plugins/docs-tools/skills/docs-orchestrator/hooks/workflow-completion-check.sh b/plugins/docs-tools/skills/docs-orchestrator/hooks/workflow-completion-check.sh index afef15c4..c36622a8 100755 --- a/plugins/docs-tools/skills/docs-orchestrator/hooks/workflow-completion-check.sh +++ b/plugins/docs-tools/skills/docs-orchestrator/hooks/workflow-completion-check.sh @@ -1,8 +1,10 @@ #!/bin/bash # workflow-completion-check.sh # -# Stop hook: blocks Claude from stopping while a workflow is still running. -# Checks each progress file for incomplete steps. +# Stop hook: blocks Claude from stopping while the ACTIVE workflow +# is still running. Only checks the workflow identified by the +# .active-workflow marker — stale workflows from other sessions +# are ignored. # # Exit codes: # 0 = allow stop @@ -19,64 +21,76 @@ if ! cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null; then exit 2 fi -# Look for progress files -shopt -s nullglob -PROGRESS_FILES=(.claude/docs/*/workflow/*.json) -shopt -u nullglob -if [ ${#PROGRESS_FILES[@]} -eq 0 ]; then +MARKER=".claude/docs/.active-workflow" + +# No marker → no active workflow → allow stop +if [ ! -f "$MARKER" ]; then + exit 0 +fi + +# Read the marker +PROGRESS_FILE=$(jq -r '.progress_file // empty' "$MARKER" 2>/dev/null) +TICKET=$(jq -r '.ticket // empty' "$MARKER" 2>/dev/null) + +# Marker is malformed or empty → clean up and allow stop +if [ -z "$PROGRESS_FILE" ] || [ -z "$TICKET" ]; then + rm -f "$MARKER" + exit 0 +fi + +# Progress file doesn't exist → stale marker → clean up and allow stop +if [ ! -f "$PROGRESS_FILE" ]; then + rm -f "$MARKER" + exit 0 +fi + +# Check the workflow status — only block for in_progress workflows +WORKFLOW_STATUS=$(jq -r '.status' "$PROGRESS_FILE" 2>/dev/null) + +if [ "$WORKFLOW_STATUS" != "in_progress" ]; then + rm -f "$MARKER" exit 0 fi -for pfile in "${PROGRESS_FILES[@]}"; do - WORKFLOW_STATUS=$(jq -r '.status' "$pfile" 2>/dev/null) - - # Skip workflows that aren't running - if [ "$WORKFLOW_STATUS" != "in_progress" ]; then - continue - fi - - TICKET=$(jq -r '.ticket' "$pfile") - WORKFLOW_TYPE=$(jq -r '.workflow_type' "$pfile") - - # Anti-loop guard: per-workflow counter prevents infinite blocking. - COUNTER_FILE="${pfile}.stop_count" - if [ -f "$COUNTER_FILE" ]; then - COUNT=$(cat "$COUNTER_FILE") - else - COUNT=0 - fi - if [ "$COUNT" -ge 5 ]; then - rm -f "$COUNTER_FILE" - continue - fi - - # Get step order from the progress file - mapfile -t STEP_ORDER < <(jq -r '.step_order[]' "$pfile" 2>/dev/null) - - if [ ${#STEP_ORDER[@]} -eq 0 ]; then - # Fall back to alphabetical key order - mapfile -t STEP_ORDER < <(jq -r '.steps | keys[]' "$pfile" 2>/dev/null) - fi - - # Find the first incomplete step - NEXT_STEP="" - for step in "${STEP_ORDER[@]}"; do - STEP_STATUS=$(jq -r --arg s "$step" '.steps[$s].status // "missing"' "$pfile") - case "$STEP_STATUS" in - completed|skipped|deferred) continue ;; - *) NEXT_STEP="$step"; break ;; - esac - done - - if [ -n "$NEXT_STEP" ]; then - echo "$((COUNT + 1))" > "$COUNTER_FILE" - echo "Documentation workflow '$WORKFLOW_TYPE' for $TICKET is not complete. Next step: $NEXT_STEP. Continue the workflow." >&2 - exit 2 - fi - - # All steps done — clean up counter +WORKFLOW_TYPE=$(jq -r '.workflow_type' "$PROGRESS_FILE" 2>/dev/null) + +# Anti-loop guard: per-workflow counter prevents infinite blocking +COUNTER_FILE="${PROGRESS_FILE}.stop_count" +if [ -f "$COUNTER_FILE" ]; then + COUNT=$(cat "$COUNTER_FILE") +else + COUNT=0 +fi +if [ "$COUNT" -ge 5 ]; then rm -f "$COUNTER_FILE" + rm -f "$MARKER" + exit 0 +fi + +# Get step order from the progress file +mapfile -t STEP_ORDER < <(jq -r '.step_order[]' "$PROGRESS_FILE" 2>/dev/null) + +if [ ${#STEP_ORDER[@]} -eq 0 ]; then + mapfile -t STEP_ORDER < <(jq -r '.steps | keys[]' "$PROGRESS_FILE" 2>/dev/null) +fi + +# Find the first incomplete step +NEXT_STEP="" +for step in "${STEP_ORDER[@]}"; do + STEP_STATUS=$(jq -r --arg s "$step" '.steps[$s].status // "missing"' "$PROGRESS_FILE") + case "$STEP_STATUS" in + completed|skipped|deferred) continue ;; + *) NEXT_STEP="$step"; break ;; + esac done -# No incomplete workflows found — allow stop +if [ -n "$NEXT_STEP" ]; then + echo "$((COUNT + 1))" > "$COUNTER_FILE" + echo "Documentation workflow '$WORKFLOW_TYPE' for $TICKET is not complete. Next step: $NEXT_STEP. Continue the workflow." >&2 + exit 2 +fi + +# All steps done — clean up and allow stop +rm -f "$COUNTER_FILE" +rm -f "$MARKER" exit 0 From 710fbc9f2f6574376f4d9ff7d3b35ac86de476d6 Mon Sep 17 00:00:00 2001 From: Aidan Reilly Date: Fri, 1 May 2026 11:02:27 +0100 Subject: [PATCH 2/3] chore: bump docs-tools plugin version to 0.0.58 Co-Authored-By: Claude Opus 4.6 rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- plugins/docs-tools/.claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/docs-tools/.claude-plugin/plugin.json b/plugins/docs-tools/.claude-plugin/plugin.json index 60f78911..513bfab5 100644 --- a/plugins/docs-tools/.claude-plugin/plugin.json +++ b/plugins/docs-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "docs-tools", - "version": "0.0.57", + "version": "0.0.58", "description": "Documentation review, writing, and workflow tools for Red Hat AsciiDoc and Markdown documentation.", "author": { "name": "Red Hat Documentation Team", From c70c763e355d84bb602255d8fa1e4136ed9274d4 Mon Sep 17 00:00:00 2001 From: Aidan Reilly Date: Fri, 1 May 2026 11:21:37 +0100 Subject: [PATCH 3/3] fix: fail closed on marker parse errors, use ticket-lower in progress path Stop hook now checks jq exit status when parsing .active-workflow marker. Parse failures block stop (exit 2) instead of silently deleting the marker. Also aligns the SKILL.md progress_file example with the lowercase ticket directory convention. Co-Authored-By: Claude Opus 4.6 rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- plugins/docs-tools/skills/docs-orchestrator/SKILL.md | 2 +- .../hooks/workflow-completion-check.sh | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/docs-tools/skills/docs-orchestrator/SKILL.md b/plugins/docs-tools/skills/docs-orchestrator/SKILL.md index da6be344..930a42b4 100644 --- a/plugins/docs-tools/skills/docs-orchestrator/SKILL.md +++ b/plugins/docs-tools/skills/docs-orchestrator/SKILL.md @@ -338,7 +338,7 @@ Write the marker file using the Write tool at the same time as creating or updat { "ticket": "", "workflow_type": "", - "progress_file": ".claude/docs//workflow/_.json" + "progress_file": ".claude/docs//workflow/_.json" } ``` diff --git a/plugins/docs-tools/skills/docs-orchestrator/hooks/workflow-completion-check.sh b/plugins/docs-tools/skills/docs-orchestrator/hooks/workflow-completion-check.sh index c36622a8..499db593 100755 --- a/plugins/docs-tools/skills/docs-orchestrator/hooks/workflow-completion-check.sh +++ b/plugins/docs-tools/skills/docs-orchestrator/hooks/workflow-completion-check.sh @@ -28,11 +28,18 @@ if [ ! -f "$MARKER" ]; then exit 0 fi -# Read the marker +# Read the marker — fail closed on parse errors PROGRESS_FILE=$(jq -r '.progress_file // empty' "$MARKER" 2>/dev/null) +JQ_RC_PF=$? TICKET=$(jq -r '.ticket // empty' "$MARKER" 2>/dev/null) +JQ_RC_TK=$? -# Marker is malformed or empty → clean up and allow stop +if [ "$JQ_RC_PF" -ne 0 ] || [ "$JQ_RC_TK" -ne 0 ]; then + echo "Failed to parse $MARKER; refusing to stop (fail closed)." >&2 + exit 2 +fi + +# Marker parsed successfully but fields are empty → stale marker → clean up if [ -z "$PROGRESS_FILE" ] || [ -z "$TICKET" ]; then rm -f "$MARKER" exit 0