diff --git a/plugins/docs-tools/.claude-plugin/plugin.json b/plugins/docs-tools/.claude-plugin/plugin.json index 09e90525..c7dd92cd 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.61", + "version": "0.0.62", "description": "Documentation review, writing, and workflow tools for Red Hat AsciiDoc and Markdown documentation.", "author": { "name": "Red Hat Documentation Team", diff --git a/plugins/docs-tools/skills/docs-orchestrator/SKILL.md b/plugins/docs-tools/skills/docs-orchestrator/SKILL.md index 88677a30..43269f2e 100644 --- a/plugins/docs-tools/skills/docs-orchestrator/SKILL.md +++ b/plugins/docs-tools/skills/docs-orchestrator/SKILL.md @@ -275,7 +275,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` @@ -329,6 +329,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`. @@ -342,7 +383,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 @@ -405,7 +448,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"` @@ -484,7 +527,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..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 @@ -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,83 @@ 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 — 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=$? + +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 +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