From 9898c085602897406a22d7fd26686cbad3fd2c39 Mon Sep 17 00:00:00 2001 From: Pierre Wizla <4233866+pwizla@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:21:17 +0200 Subject: [PATCH 1/3] Split self-healing into 2-step Haiku Router + Sonnet Drafter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Haiku runs the Router for ~$0.02/run. If no targets are found, Sonnet is never invoked — saving ~$0.20 per "nothing to do" run. When targets exist, Sonnet handles the full Drafter pipeline. - New: docs-self-healing-router.md (Haiku prompt) - New: docs-self-healing-drafter.md (Sonnet prompt) - Deleted: docs-self-healing.md (replaced by the two above) - Workflow: two claude-code-action steps with bash bridge - Summary: 3-state (no PRs / Router skip / Drafter results) Co-Authored-By: Claude Opus 4.6 (1M context) --- ...ealing.md => docs-self-healing-drafter.md} | 92 +++--------- .github/prompts/docs-self-healing-router.md | 69 +++++++++ .github/workflows/docs-self-healing.yml | 133 ++++++++++++++---- 3 files changed, 193 insertions(+), 101 deletions(-) rename .github/prompts/{docs-self-healing.md => docs-self-healing-drafter.md} (52%) create mode 100644 .github/prompts/docs-self-healing-router.md diff --git a/.github/prompts/docs-self-healing.md b/.github/prompts/docs-self-healing-drafter.md similarity index 52% rename from .github/prompts/docs-self-healing.md rename to .github/prompts/docs-self-healing-drafter.md index 2e906db141..0e7861840c 100644 --- a/.github/prompts/docs-self-healing.md +++ b/.github/prompts/docs-self-healing-drafter.md @@ -1,75 +1,35 @@ -# Strapi Documentation Self-Healing Agent +# Self-Healing Drafter (Sonnet) You are running in automated mode inside a GitHub Actions workflow on `strapi/documentation`. -Your job is to process each PR merged into `strapi/strapi` in the last 24 hours and open a draft documentation PR on `strapi/documentation` for every one that requires a doc update. +The Router has already analyzed each PR and identified documentation targets. +Your job is to draft the content, create branches, and open draft PRs. ## Environment -- `$STRAPI_SOURCE` — local checkout of `strapi/strapi` (read-only, for diffs) -- `$DOC_REPO` — local checkout of `strapi/documentation` (read + write, for creating PRs) -- `$FILTERED_PRS` — JSON array of pre-filtered PRs (chores, CI, deps, tests already excluded by the workflow) +- `$DOC_REPO` — local checkout of `strapi/documentation` (read + write) +- `$ROUTER_RESULTS` — JSON with routing decisions from the Haiku Router step +- Pre-fetched diffs and bodies in `/tmp/pr--body.txt` and `/tmp/pr-.diff` - GitHub CLI (`gh`) is authenticated via `GH_TOKEN` -- Model: `claude-sonnet-4-6` (set in the workflow YAML; optimized for cost on batch automation) -## Step 1 — Read the pre-filtered PR list +## Step 1 — Parse Router results -The workflow has already fetched and filtered merged PRs. The list is in `$FILTERED_PRS` as a JSON array: +The Router results are in `$ROUTER_RESULTS` as a JSON string. Parse it. +Only process PRs where `decision` is `"has_targets"`. Skip the rest. -```json -[{"number": 12345, "title": "feat: add feature X", "html_url": "https://github.com/strapi/strapi/pull/12345"}, ...] -``` - -Parse this list. **Do NOT re-fetch PRs from the GitHub API** — the workflow already did that. - -**Rate limit:** Process a maximum of 1 PR per run (testing mode). If more qualify, log the skipped ones -to stdout and they will be picked up on the next run. - -## Step 2 — Read pre-fetched PR context (per PR) - -The workflow has already fetched the body and diff for each PR. Read them from: - -- `/tmp/pr--body.txt` — PR description -- `/tmp/pr-.diff` — full diff - -**Do NOT fetch these from the GitHub API** — they are already on disk. - -**Diff size threshold:** If the diff exceeds 3000 lines, skip this PR and log: -"PR # skipped — diff too large (X lines), flag for manual /autodoc". - -## Step 3 — Run the Router (per PR) +Each PR with targets includes a `targets_yaml` field — this is the Router's full YAML output +containing `targets`, `doc_type`, `template`, `guide`, and `confidence`. -**Read these files once at the start of the run** (not per PR): -- Router prompt: `$DOC_REPO/agents/prompts/router.md` -- Sidebars: `$DOC_REPO/docusaurus/sidebars.js` -- Page index: `$DOC_REPO/docusaurus/static/llms.txt` +## Step 2 — Run the documentation pipeline (per PR with targets) -Then, for each PR, apply the Router logic using: -- PR title and description from Step 2 -- The diff from Step 2 - -The Router will produce a YAML `targets` block. - -**Skip the PR if:** -- The Router finds no targets -- The Router sets `ask_user` (log the question to stdout for manual handling) - -Note: chores, CI, deps, tests, and translations are already filtered out by the workflow -before Claude runs. The Router only sees PRs that passed the pre-filter. - -**If the Router finds no targets for ANY PR, skip Step 4 entirely.** -Do not load Drafter/checker prompts unless at least one PR has targets. - -## Step 4 — Run the documentation pipeline (per PR with targets) - -For each PR where the Router identified targets, run the Create/Update Mode pipeline. - -**Load these agent prompts now** (not before — only if Step 3 found targets): +**Load these agent prompts now:** - Orchestrator: `$DOC_REPO/agents/prompts/orchestrator.md` - Outline Generator: `$DOC_REPO/agents/prompts/outline-generator.md` - Drafter: `$DOC_REPO/agents/prompts/drafter.md` - Style Checker: `$DOC_REPO/agents/prompts/style-checker.md` - Integrity Checker: `$DOC_REPO/agents/prompts/integrity-checker.md` +For each PR, read the pre-fetched body and diff from `/tmp/pr--body.txt` and `/tmp/pr-.diff`. + Follow the auto-chain execution from the Orchestrator: 1. **For `create_page` targets:** @@ -93,13 +53,12 @@ Follow the auto-chain execution from the Orchestrator: - Log any issues but do not block PR creation — Pierre will verify during review **Authoring guides:** For each target, load the relevant authoring guide from `$DOC_REPO/agents/authoring/` -based on the Router's `doc_type` and target path. These contain section-specific conventions. -Authoring guides are small and target-specific — read them per target, not upfront. +based on the Router's `doc_type` and target path. Read per target, not upfront. **Templates:** For `create_page` targets, load the relevant template from `$DOC_REPO/agents/templates/` based on the Router's `doc_type`. -## Step 5 — Create branch and draft PR (per PR) +## Step 3 — Create branch and draft PR (per PR) After the Drafter has produced output for all targets: @@ -113,7 +72,6 @@ cd $DOC_REPO BRANCH_NAME="/" git checkout -b "$BRANCH_NAME" -# (apply all Drafter outputs to the appropriate files) git add . git commit -m " # Imperative mood, no prefix, describe the doc change. Max 80 chars." @@ -146,26 +104,22 @@ git clean -fd git reset --hard origin/main ``` -## Step 6 — Write run summary +## Step 4 — Write run summary -After processing all PRs (or if none qualify), write a JSON summary to `/tmp/self-healing-summary.json`: +Write a JSON summary to `/tmp/self-healing-summary.json`: ```json { "processed": [ {"number": 12345, "title": "Add feature X", "doc_pr": "https://github.com/strapi/documentation/pull/99", "branch": "cms/add-feature-x"} ], - "skipped": [ - {"number": 12346, "title": "Fix typo in test", "reason": "Router: no doc update needed"}, - {"number": 12347, "title": "Massive refactor", "reason": "Diff too large (4200 lines)"} - ], "errors": [ {"number": 12348, "title": "Add plugin Y", "error": "Drafter failed after retry"} ] } ``` -**Always write this file**, even if all arrays are empty. The workflow reads it to build the job summary. +**Always write this file**, even if all arrays are empty. ## Rules @@ -173,9 +127,7 @@ After processing all PRs (or if none qualify), write a JSON summary to `/tmp/sel - **Only modify files in `$DOC_REPO/docusaurus/docs/`** and `$DOC_REPO/docusaurus/static/` (for images) - **Follow all conventions** in `$DOC_REPO/agents/` — the Router and authoring guides are the source of truth - **Follow git-rules.md** — branch naming (`/cms`, `/cloud`, `/repo`), commit messages (imperative, no prefix), PR titles -- **If the Router sets `ask_user`:** skip this PR and log the question to stdout -- **If no PR requires a doc update:** exit cleanly without creating anything -- **Max 5 PRs per run** — log skipped PRs to stdout +- **If no PR has targets:** exit cleanly without creating anything - **Max 3000 lines per diff** — skip and log oversized diffs - **Never modify workflow files, configuration files, or sidebars.js** -- **NEVER run any write operation on strapi/strapi** — no issues, no comments, no PRs, no pushes, no API calls that modify state. The GH_TOKEN has write access but this workflow ONLY writes to strapi/documentation. Read-only access to strapi/strapi (diffs, PR bodies) is the only permitted use. +- **NEVER run any write operation on strapi/strapi** — no issues, no comments, no PRs, no pushes, no API calls that modify state. Read-only access to strapi/strapi (diffs, PR bodies) is the only permitted use. diff --git a/.github/prompts/docs-self-healing-router.md b/.github/prompts/docs-self-healing-router.md new file mode 100644 index 0000000000..d859b9a40b --- /dev/null +++ b/.github/prompts/docs-self-healing-router.md @@ -0,0 +1,69 @@ +# Self-Healing Router (Haiku) + +You are a lightweight routing agent. Your ONLY job is to decide, for each PR, +whether documentation needs updating and what targets to hit. + +You run on Haiku for cost efficiency. Do NOT draft content or create PRs. + +## Environment + +- `$DOC_REPO` — local checkout of `strapi/documentation` +- `$FILTERED_PRS` — JSON array of pre-filtered PRs (chores/CI/deps/tests already excluded) +- Pre-fetched diffs and bodies in `/tmp/pr--body.txt` and `/tmp/pr-.diff` + +## Instructions + +1. **Read these files once:** + - Router prompt: `$DOC_REPO/agents/prompts/router.md` + - Sidebars: `$DOC_REPO/docusaurus/sidebars.js` + - Page index: `$DOC_REPO/docusaurus/static/llms.txt` + +2. **Parse `$FILTERED_PRS`** to get the list of PRs. + +3. **For each PR:** + - Read `/tmp/pr--body.txt` and `/tmp/pr-.diff` + - If the diff exceeds 3000 lines, mark as `skipped` with reason "Diff too large" + - Otherwise, apply the Router logic to decide if docs need updating + +4. **Write the routing result** to `/tmp/router-results.json` using this exact schema: + +```json +{ + "prs": [ + { + "number": 12345, + "title": "feat: add feature X", + "decision": "has_targets", + "reason": "", + "targets_yaml": "targets:\n - path: cms/features/x.md\n action: update_section\n priority: primary\n existing_section: \"Configuration\"\n description: \"Add feature X config options\"\n\ndoc_type: feature\ntemplate: null\nguide: agents/authoring/AGENTS.cms.features.md\nconfidence: high" + }, + { + "number": 12346, + "title": "fix(admin): internal race condition fix", + "decision": "skip", + "reason": "Internal admin UI bug fix, no public API or behavior change", + "targets_yaml": "" + }, + { + "number": 12347, + "title": "enhancement: add new CLI command", + "decision": "ask_user", + "reason": "Uncertain whether this CLI command is public-facing or internal tooling", + "targets_yaml": "" + } + ] +} +``` + +**`decision` must be one of:** `has_targets`, `skip`, `ask_user` + +**`targets_yaml`** is the full Router YAML output (as a string), only when `decision` is `has_targets`. Include `doc_type`, `template`, `guide`, `confidence`, and the full `targets` block. + +## Rules + +- **Do NOT read any agent prompts except `router.md`** +- **Do NOT read or modify any documentation files** +- **Do NOT create branches, commits, or PRs** +- **ONLY read diffs, the Router prompt, sidebars.js, llms.txt, and write the result file** +- **Max 1 PR per run** (testing mode). Log extras to stdout. +- **NEVER run any write operation on strapi/strapi** diff --git a/.github/workflows/docs-self-healing.yml b/.github/workflows/docs-self-healing.yml index 7e7890ca90..9717955e4d 100644 --- a/.github/workflows/docs-self-healing.yml +++ b/.github/workflows/docs-self-healing.yml @@ -153,32 +153,107 @@ jobs: fi done - - name: Load prompt from file + # ── Step A: Haiku Router (cheap, fast) ── + - name: Load Router prompt if: steps.check-prs.outputs.has_prs == 'true' - id: load-prompt + id: load-router-prompt run: | - PROMPT=$(cat .github/prompts/docs-self-healing.md) - # Write to file for multiline support - echo "$PROMPT" > /tmp/self-healing-prompt.txt - # Use delimiter for multiline output echo "prompt<> $GITHUB_OUTPUT - cat .github/prompts/docs-self-healing.md >> $GITHUB_OUTPUT + cat .github/prompts/docs-self-healing-router.md >> $GITHUB_OUTPUT echo "PROMPT_EOF" >> $GITHUB_OUTPUT - - name: Run Claude doc agent + - name: Run Router (Haiku) if: steps.check-prs.outputs.has_prs == 'true' + id: router uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_SELF_HEALING_DOCS_API_KEY }} github_token: ${{ secrets.PAT_TOKEN_PIWI }} - prompt: ${{ steps.load-prompt.outputs.prompt }} + prompt: ${{ steps.load-router-prompt.outputs.prompt }} + claude_args: '--model claude-haiku-4-5-20251001 --max-turns 15 --allowedTools "Bash,Read,Glob,Grep"' + show_full_output: true + env: + DOC_REPO: ${{ github.workspace }} + FILTERED_PRS: ${{ steps.check-prs.outputs.pr_list }} + + - name: Check Router results + if: steps.check-prs.outputs.has_prs == 'true' + id: check-router + run: | + RESULTS_FILE="/tmp/router-results.json" + if [ ! -f "$RESULTS_FILE" ]; then + echo "Router did not produce results file" + echo "has_targets=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Count PRs with targets + TARGETS=$(jq '[.prs[] | select(.decision == "has_targets")] | length' "$RESULTS_FILE") + SKIPPED=$(jq '[.prs[] | select(.decision == "skip")] | length' "$RESULTS_FILE") + ASK_USER=$(jq '[.prs[] | select(.decision == "ask_user")] | length' "$RESULTS_FILE") + + echo "Router results: $TARGETS with targets, $SKIPPED skipped, $ASK_USER ask_user" + + # Log Router decisions in summary + echo "### Router decisions (Haiku)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$TARGETS" -gt 0 ]; then + echo "**Needs docs ($TARGETS):**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + jq -r '.prs[] | select(.decision == "has_targets") | "- **#\(.number) — \(.title)**"' "$RESULTS_FILE" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$SKIPPED" -gt 0 ]; then + echo "**No docs needed ($SKIPPED):**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + jq -r '.prs[] | select(.decision == "skip") | "- ~~#\(.number) — \(.title)~~ — \(.reason)"' "$RESULTS_FILE" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$ASK_USER" -gt 0 ]; then + echo "**Needs manual review ($ASK_USER):**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + jq -r '.prs[] | select(.decision == "ask_user") | "- ⚠️ #\(.number) — \(.title) — \(.reason)"' "$RESULTS_FILE" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$TARGETS" -eq 0 ]; then + echo "has_targets=false" >> $GITHUB_OUTPUT + else + echo "has_targets=true" >> $GITHUB_OUTPUT + fi + + # Pass router results to Sonnet + { + echo "router_results<> $GITHUB_OUTPUT + + # ── Step B: Sonnet Drafter (only if Router found targets) ── + - name: Load Drafter prompt + if: steps.check-router.outputs.has_targets == 'true' + id: load-drafter-prompt + run: | + echo "prompt<> $GITHUB_OUTPUT + cat .github/prompts/docs-self-healing-drafter.md >> $GITHUB_OUTPUT + echo "PROMPT_EOF" >> $GITHUB_OUTPUT + + - name: Run Drafter (Sonnet) + if: steps.check-router.outputs.has_targets == 'true' + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_SELF_HEALING_DOCS_API_KEY }} + github_token: ${{ secrets.PAT_TOKEN_PIWI }} + prompt: ${{ steps.load-drafter-prompt.outputs.prompt }} claude_args: '--model claude-sonnet-4-6 --max-turns 25 --allowedTools "Bash,Read,Write,Edit,Glob,Grep"' show_full_output: true env: - STRAPI_SOURCE: ${{ github.workspace }}/.strapi-source DOC_REPO: ${{ github.workspace }} GH_TOKEN: ${{ secrets.PAT_TOKEN_PIWI }} - FILTERED_PRS: ${{ steps.check-prs.outputs.pr_list }} + ROUTER_RESULTS: ${{ steps.check-router.outputs.router_results }} - name: Summary if: always() @@ -189,45 +264,41 @@ jobs: echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + # Case 1: No PRs passed pre-filter if [ "${{ steps.check-prs.outputs.has_prs }}" == "false" ]; then - echo "**Result:** No merged PRs in the last 24h. Nothing to process." >> $GITHUB_STEP_SUMMARY + echo "**Result:** No candidate PRs after pre-filtering. Sonnet was not invoked." >> $GITHUB_STEP_SUMMARY exit 0 fi + # Case 2: Router ran but found no targets — Sonnet was NOT invoked + if [ "${{ steps.check-router.outputs.has_targets }}" != "true" ]; then + echo "**Result:** Router (Haiku) found no documentation targets. Sonnet was not invoked." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Case 3: Sonnet ran — read its summary SUMMARY_FILE="/tmp/self-healing-summary.json" if [ ! -f "$SUMMARY_FILE" ]; then - echo "**Result:** Claude agent ran but no summary file was produced. Check the log artifact." >> $GITHUB_STEP_SUMMARY + echo "**Result:** Sonnet ran but no summary file was produced. Check logs." >> $GITHUB_STEP_SUMMARY exit 0 fi - # PRs processed (doc PRs created) + # Drafter results + echo "### Drafter results (Sonnet)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + PROCESSED=$(jq -r '.processed | length' "$SUMMARY_FILE") if [ "$PROCESSED" -gt 0 ]; then - echo "### PRs processed ($PROCESSED)" >> $GITHUB_STEP_SUMMARY + echo "**Doc PRs created ($PROCESSED):**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY jq -r '.processed[] | "- [strapi/strapi#\(.number) — \(.title)](\(.doc_pr))"' "$SUMMARY_FILE" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY fi - # PRs skipped - SKIPPED=$(jq -r '.skipped | length' "$SUMMARY_FILE") - if [ "$SKIPPED" -gt 0 ]; then - echo "### PRs skipped ($SKIPPED)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - jq -r '.skipped[] | "- strapi/strapi#\(.number) — \(.title): \(.reason)"' "$SUMMARY_FILE" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # Errors ERRORS=$(jq -r '.errors | length' "$SUMMARY_FILE") if [ "$ERRORS" -gt 0 ]; then - echo "### Errors ($ERRORS)" >> $GITHUB_STEP_SUMMARY + echo "**Errors ($ERRORS):**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY jq -r '.errors[] | "- ⚠️ strapi/strapi#\(.number) — \(.title): \(.error)"' "$SUMMARY_FILE" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY fi - - # Totals - TOTAL=$((PROCESSED + SKIPPED + ERRORS)) - echo "---" >> $GITHUB_STEP_SUMMARY - echo "**Total PRs scanned:** $TOTAL | **Doc PRs created:** $PROCESSED | **Skipped:** $SKIPPED | **Errors:** $ERRORS" >> $GITHUB_STEP_SUMMARY From 06dd782a6ba587ee7d4531dfab37d26944e621d8 Mon Sep 17 00:00:00 2001 From: Pierre Wizla <4233866+pwizla@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:23:56 +0200 Subject: [PATCH 2/3] Let Haiku handle micro-edits directly without invoking Sonnet Router now outputs a complexity field (micro/full). Micro-edits (add_link, add_mention, add_tip) are executed by Haiku directly including branch creation and PR. Sonnet is only invoked for full-complexity targets (create_page, update_section, add_section). Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/prompts/docs-self-healing-router.md | 72 ++++++++++++++++++++- .github/workflows/docs-self-healing.yml | 31 ++++++--- 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/.github/prompts/docs-self-healing-router.md b/.github/prompts/docs-self-healing-router.md index d859b9a40b..6142186161 100644 --- a/.github/prompts/docs-self-healing-router.md +++ b/.github/prompts/docs-self-healing-router.md @@ -34,13 +34,23 @@ You run on Haiku for cost efficiency. Do NOT draft content or create PRs. "number": 12345, "title": "feat: add feature X", "decision": "has_targets", + "complexity": "full", "reason": "", "targets_yaml": "targets:\n - path: cms/features/x.md\n action: update_section\n priority: primary\n existing_section: \"Configuration\"\n description: \"Add feature X config options\"\n\ndoc_type: feature\ntemplate: null\nguide: agents/authoring/AGENTS.cms.features.md\nconfidence: high" }, + { + "number": 12350, + "title": "fix: add missing link to REST API page", + "decision": "has_targets", + "complexity": "micro", + "reason": "", + "targets_yaml": "targets:\n - path: cms/api/rest.md\n action: add_link\n priority: optional\n description: \"Add link to new filtering guide\"\n\ndoc_type: api\nconfidence: high" + }, { "number": 12346, "title": "fix(admin): internal race condition fix", "decision": "skip", + "complexity": "", "reason": "Internal admin UI bug fix, no public API or behavior change", "targets_yaml": "" }, @@ -48,6 +58,7 @@ You run on Haiku for cost efficiency. Do NOT draft content or create PRs. "number": 12347, "title": "enhancement: add new CLI command", "decision": "ask_user", + "complexity": "", "reason": "Uncertain whether this CLI command is public-facing or internal tooling", "targets_yaml": "" } @@ -57,13 +68,68 @@ You run on Haiku for cost efficiency. Do NOT draft content or create PRs. **`decision` must be one of:** `has_targets`, `skip`, `ask_user` +**`complexity`** (only when `decision` is `has_targets`): +- `"micro"` — ALL targets are micro-edits (`add_link`, `add_mention`, `add_tip`). Haiku can handle these. +- `"full"` — at least one target is `create_page`, `update_section`, `add_section`, or `create_category`. Requires Sonnet. + **`targets_yaml`** is the full Router YAML output (as a string), only when `decision` is `has_targets`. Include `doc_type`, `template`, `guide`, `confidence`, and the full `targets` block. +## Step 5 — Execute micro-edits (if all targets are micro) + +If a PR has `complexity: "micro"`, you handle the full pipeline yourself — no Sonnet needed. + +For each micro target (`add_link`, `add_mention`, `add_tip`): +1. Read the target file from `$DOC_REPO/docusaurus/docs/` +2. Apply the edit (add the link, mention, or tip) +3. Write the modified file back + +Then create the branch and PR: + +```bash +cd $DOC_REPO +BRANCH_NAME="/" +git checkout -b "$BRANCH_NAME" +git add . +git commit -m "" +git push -u origin "$BRANCH_NAME" +gh pr create \ + --repo strapi/documentation \ + --title "[Docs self-healing] " \ + --body "$(cat <<'BODY' +This PR updates documentation based on https://github.com/strapi/strapi/pull/. + +Generated automatically by the docs self-healing workflow (micro-edit, Haiku). +Review before merging. +BODY +)" \ + --draft \ + --label "auto-doc-healing" +git checkout main +git clean -fd +git reset --hard origin/main +``` + +**Title rules:** `[Docs self-healing]` prefix, imperative mood, no conventional prefix, describe the doc change. + +After micro-edits, add the PR to the results file with `decision: "has_targets"` and record the doc PR URL. + +Update `/tmp/router-results.json` to include a `doc_pr` field for micro PRs you handled: + +```json +{ + "number": 12350, + "decision": "has_targets", + "complexity": "micro", + "doc_pr": "https://github.com/strapi/documentation/pull/99", + ... +} +``` + ## Rules - **Do NOT read any agent prompts except `router.md`** -- **Do NOT read or modify any documentation files** -- **Do NOT create branches, commits, or PRs** -- **ONLY read diffs, the Router prompt, sidebars.js, llms.txt, and write the result file** +- **For micro-edits only:** you may read and modify documentation files and create branches/PRs +- **For full complexity:** do NOT modify files or create PRs — leave that for Sonnet +- **ONLY read diffs, the Router prompt, sidebars.js, llms.txt, and write the result file** (plus doc files for micro-edits) - **Max 1 PR per run** (testing mode). Log extras to stdout. - **NEVER run any write operation on strapi/strapi** diff --git a/.github/workflows/docs-self-healing.yml b/.github/workflows/docs-self-healing.yml index 9717955e4d..f88d87a3fd 100644 --- a/.github/workflows/docs-self-healing.yml +++ b/.github/workflows/docs-self-healing.yml @@ -170,10 +170,11 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_SELF_HEALING_DOCS_API_KEY }} github_token: ${{ secrets.PAT_TOKEN_PIWI }} prompt: ${{ steps.load-router-prompt.outputs.prompt }} - claude_args: '--model claude-haiku-4-5-20251001 --max-turns 15 --allowedTools "Bash,Read,Glob,Grep"' + claude_args: '--model claude-haiku-4-5-20251001 --max-turns 20 --allowedTools "Bash,Read,Write,Edit,Glob,Grep"' show_full_output: true env: DOC_REPO: ${{ github.workspace }} + GH_TOKEN: ${{ secrets.PAT_TOKEN_PIWI }} FILTERED_PRS: ${{ steps.check-prs.outputs.pr_list }} - name: Check Router results @@ -187,21 +188,30 @@ jobs: exit 0 fi - # Count PRs with targets - TARGETS=$(jq '[.prs[] | select(.decision == "has_targets")] | length' "$RESULTS_FILE") + # Count PRs by decision and complexity + MICRO=$(jq '[.prs[] | select(.decision == "has_targets" and .complexity == "micro")] | length' "$RESULTS_FILE") + FULL=$(jq '[.prs[] | select(.decision == "has_targets" and .complexity == "full")] | length' "$RESULTS_FILE") SKIPPED=$(jq '[.prs[] | select(.decision == "skip")] | length' "$RESULTS_FILE") ASK_USER=$(jq '[.prs[] | select(.decision == "ask_user")] | length' "$RESULTS_FILE") + TARGETS=$((MICRO + FULL)) - echo "Router results: $TARGETS with targets, $SKIPPED skipped, $ASK_USER ask_user" + echo "Router results: $TARGETS with targets ($MICRO micro, $FULL full), $SKIPPED skipped, $ASK_USER ask_user" # Log Router decisions in summary echo "### Router decisions (Haiku)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - if [ "$TARGETS" -gt 0 ]; then - echo "**Needs docs ($TARGETS):**" >> $GITHUB_STEP_SUMMARY + if [ "$MICRO" -gt 0 ]; then + echo "**Micro-edits handled by Haiku ($MICRO):**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + jq -r '.prs[] | select(.decision == "has_targets" and .complexity == "micro") | "- **#\(.number) — \(.title)**" + (if .doc_pr then " → [\(.doc_pr)](\(.doc_pr))" else "" end)' "$RESULTS_FILE" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$FULL" -gt 0 ]; then + echo "**Full drafting needed — sent to Sonnet ($FULL):**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - jq -r '.prs[] | select(.decision == "has_targets") | "- **#\(.number) — \(.title)**"' "$RESULTS_FILE" >> $GITHUB_STEP_SUMMARY + jq -r '.prs[] | select(.decision == "has_targets" and .complexity == "full") | "- **#\(.number) — \(.title)**"' "$RESULTS_FILE" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY fi @@ -219,20 +229,21 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY fi - if [ "$TARGETS" -eq 0 ]; then + # Sonnet only needed for "full" complexity PRs + if [ "$FULL" -eq 0 ]; then echo "has_targets=false" >> $GITHUB_OUTPUT else echo "has_targets=true" >> $GITHUB_OUTPUT fi - # Pass router results to Sonnet + # Pass router results to Sonnet (only full-complexity PRs) { echo "router_results<> $GITHUB_OUTPUT - # ── Step B: Sonnet Drafter (only if Router found targets) ── + # ── Step B: Sonnet Drafter (only if full-complexity targets found) ── - name: Load Drafter prompt if: steps.check-router.outputs.has_targets == 'true' id: load-drafter-prompt From 551519d8a9f2cbcf6ecd680b75cd78af51e11e2c Mon Sep 17 00:00:00 2001 From: Pierre Wizla <4233866+pwizla@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:28:42 +0200 Subject: [PATCH 3/3] Raise rate limit from 1 to 5 PRs per run Haiku routing is cheap enough to handle 5 PRs safely. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/prompts/docs-self-healing-router.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/prompts/docs-self-healing-router.md b/.github/prompts/docs-self-healing-router.md index 6142186161..d69daad494 100644 --- a/.github/prompts/docs-self-healing-router.md +++ b/.github/prompts/docs-self-healing-router.md @@ -131,5 +131,5 @@ Update `/tmp/router-results.json` to include a `doc_pr` field for micro PRs you - **For micro-edits only:** you may read and modify documentation files and create branches/PRs - **For full complexity:** do NOT modify files or create PRs — leave that for Sonnet - **ONLY read diffs, the Router prompt, sidebars.js, llms.txt, and write the result file** (plus doc files for micro-edits) -- **Max 1 PR per run** (testing mode). Log extras to stdout. +- **Max 5 PRs per run.** Log extras to stdout for the next run. - **NEVER run any write operation on strapi/strapi**