Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions .github/workflows/bulk-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ on:
description: "Seconds to wait between dispatches (0 = fire all at once, default 120 = 2 min)"
required: false
default: '120'
model:
description: "Claude model to use across generate / review / repair (default sonnet)"
required: false
type: choice
default: 'sonnet'
options:
- haiku
- sonnet
- opus

env:
ALL_LIBRARIES: "matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot"
Expand Down Expand Up @@ -168,13 +177,14 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MATRIX: ${{ needs.build-matrix.outputs.matrix }}
PACE_SECONDS: ${{ inputs.pace_seconds || '120' }}
MODEL: ${{ inputs.model || 'sonnet' }}
run: |
set -u

pace="${PACE_SECONDS}"
pairs=$(echo "$MATRIX" | jq -r '.include[] | "\(.specification_id) \(.library)"')
total=$(echo "$pairs" | wc -l | tr -d ' ')
echo "::notice::Dispatching $total item(s) with ${pace}s pacing between each"
echo "::notice::Dispatching $total item(s) with ${pace}s pacing between each (model=${MODEL})"

i=0
failed=0
Expand Down Expand Up @@ -203,11 +213,13 @@ jobs:
gh workflow run impl-generate.yml --repo "${{ github.repository }}" \
-f specification_id="${SPEC_ID}" \
-f library="${LIBRARY}" \
-f issue_number="${ISSUE}" && dispatched=1 && break
-f issue_number="${ISSUE}" \
-f model="${MODEL}" && dispatched=1 && break
else
gh workflow run impl-generate.yml --repo "${{ github.repository }}" \
-f specification_id="${SPEC_ID}" \
-f library="${LIBRARY}" && dispatched=1 && break
-f library="${LIBRARY}" \
-f model="${MODEL}" && dispatched=1 && break
fi
echo "::warning::Dispatch attempt $attempt failed for ${SPEC_ID}/${LIBRARY}, retrying in 10s"
sleep 10
Expand Down
52 changes: 42 additions & 10 deletions .github/workflows/daily-regen.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "Scheduled: Regen oldest specs"
run-name: "Scheduled regen (${{ github.event.inputs.count || '1' }} specs)"
run-name: "Scheduled regen (${{ github.event.inputs.specification_id || github.event.inputs.count || '1' }} / ${{ github.event.inputs.model || 'haiku' }})"

# Picks the N oldest specs (by most-recent implementation `updated` timestamp)
# and re-dispatches `bulk-generate.yml` for each. Default N=1 per cron tick.
Expand All @@ -9,21 +9,34 @@ run-name: "Scheduled regen (${{ github.event.inputs.count || '1' }} specs)"
# 02, 04, 06, 08, 10, 12, 14, 16. The 20:00 and 22:00 Berlin slots (UTC 18, 20)
# are intentionally skipped so runs never start during the user's evening.
#
# bulk-generate is serialised via its own concurrency group. With Sonnet +
# reduced bulk-generate pace, a single spec completes well within the 2h slot,
# leaving the user window clean.
# bulk-generate is serialised via its own concurrency group. Default model is
# Haiku, so a single spec completes well within the 2h slot, leaving the user
# window clean. The model can be overridden per manual run.
#
# Triggers:
# - schedule: 10× daily (UTC, every 2h except 18:00 and 20:00 UTC)
# - workflow_dispatch: manual, with inputs for count + dry-run
# - schedule: 10× daily (UTC, every 2h except 18:00 and 20:00 UTC) → Haiku
# - workflow_dispatch: manual, with inputs for spec id, model, count, dry-run

on:
schedule:
- cron: '0 0,2,4,6,8,10,12,14,16,22 * * *'
workflow_dispatch:
inputs:
specification_id:
description: "Specific spec id to regen (leave empty to pick the oldest)"
required: false
default: ''
model:
description: "Claude model to use across all generate / review / repair steps"
required: false
type: choice
default: 'haiku'
options:
- haiku
- sonnet
- opus
count:
description: "How many of the oldest specs to regen (default 1)"
description: "How many of the oldest specs to regen (ignored when specification_id is set, default 1)"
required: false
default: '1'
min_age_hours:
Expand Down Expand Up @@ -60,14 +73,16 @@ jobs:
- name: Install PyYAML
run: pip install pyyaml

- name: Pick oldest spec(s)
- name: Pick oldest spec(s) (or use override)
id: pick
env:
COUNT: ${{ inputs.count || '1' }}
MIN_AGE_HOURS: ${{ inputs.min_age_hours || '20' }}
SPEC_OVERRIDE: ${{ inputs.specification_id }}
run: |
python3 <<'PY'
import os
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

Expand All @@ -76,9 +91,24 @@ jobs:

COUNT = int(os.environ["COUNT"])
MIN_AGE = timedelta(hours=int(os.environ["MIN_AGE_HOURS"]))
OVERRIDE = (os.environ.get("SPEC_OVERRIDE") or "").strip()
NOW = datetime.now(timezone.utc)

specs_dir = Path("plots")

# Manual override: caller specified a spec id — validate and use it.
if OVERRIDE:
if not (specs_dir / OVERRIDE / "specification.md").is_file():
print(f"::error::Spec '{OVERRIDE}' not found at plots/{OVERRIDE}/specification.md")
sys.exit(1)
picks = [OVERRIDE]
print(f"::notice::Using override spec: {OVERRIDE}")
github_output = os.environ["GITHUB_OUTPUT"]
with open(github_output, "a", encoding="utf-8") as f:
f.write(f"specs={OVERRIDE}\n")
f.write(f"count=1\n")
sys.exit(0)

candidates: list[tuple[datetime, str]] = []

for spec_dir in sorted(specs_dir.iterdir()):
Expand Down Expand Up @@ -142,13 +172,15 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPECS: ${{ needs.pick.outputs.specs }}
MODEL: ${{ inputs.model || 'haiku' }}
run: |
for spec in $SPECS; do
echo "::notice::Dispatching bulk-generate for $spec (all 9 libs)"
echo "::notice::Dispatching bulk-generate for $spec (all 9 libs, model=$MODEL)"
gh workflow run bulk-generate.yml \
--repo "${{ github.repository }}" \
-f specification_id="$spec" \
-f library=all
-f library=all \
-f model="$MODEL"
# Small pause between dispatches so GitHub's webhook processing has a moment.
sleep 5
done
45 changes: 34 additions & 11 deletions .github/workflows/impl-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ on:
description: "Issue number (optional, for tracking)"
required: false
type: string
model:
description: "Claude model to use (also threaded into review + repair)"
required: false
type: choice
default: 'sonnet'
options:
- haiku
- sonnet
- opus

# Global concurrency: max 3 concurrent implementation workflows
concurrency:
Expand Down Expand Up @@ -72,12 +81,14 @@ jobs:
SPEC_ID="${{ inputs.specification_id }}"
LIBRARY="${{ inputs.library }}"
ISSUE="${{ inputs.issue_number }}"
MODEL="${{ inputs.model }}"
else
# From label trigger: generate:{library}
LIBRARY=$(echo "$LABEL_NAME" | sed 's/^generate://')
# Extract spec ID from issue title: [spec-id] ...
SPEC_ID=$(echo "$ISSUE_TITLE" | sed -n 's/^\[\([a-z0-9-]*\)\].*/\1/p')
ISSUE="${{ github.event.issue.number }}"
MODEL=""
fi

if [ -z "$SPEC_ID" ]; then
Expand All @@ -90,15 +101,21 @@ jobs:
exit 1
fi

# Default model when caller didn't supply one (label trigger path).
if [ -z "$MODEL" ]; then
MODEL="sonnet"
fi

# Language: only python supported today. Future multi-language work will derive this per spec.
LANGUAGE="python"

echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT
echo "library=$LIBRARY" >> $GITHUB_OUTPUT
echo "language=$LANGUAGE" >> $GITHUB_OUTPUT
echo "issue_number=$ISSUE" >> $GITHUB_OUTPUT
echo "model=$MODEL" >> $GITHUB_OUTPUT

echo "::notice::Generating $LANGUAGE/$LIBRARY for $SPEC_ID (issue: ${ISSUE:-none})"
echo "::notice::Generating $LANGUAGE/$LIBRARY for $SPEC_ID (issue: ${ISSUE:-none}, model: ${MODEL})"

- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand Down Expand Up @@ -308,7 +325,7 @@ jobs:
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--model sonnet"
claude_args: "--model ${{ steps.inputs.outputs.model }}"
# bulk-generate dispatches us from the github-actions bot; explicitly allow it.
allowed_bots: '*'
prompt: |
Expand All @@ -327,7 +344,7 @@ jobs:
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--model sonnet"
claude_args: "--model ${{ steps.inputs.outputs.model }}"
# bulk-generate dispatches us from the github-actions bot; explicitly allow it.
allowed_bots: '*'
prompt: |
Expand All @@ -350,6 +367,7 @@ jobs:
LIBRARY: ${{ steps.inputs.outputs.library }}
ISSUE: ${{ steps.issue.outputs.number }}
BRANCH: ${{ steps.branch.outputs.branch }}
MODEL: ${{ steps.inputs.outputs.model }}
run: |
IMPL_DIR="plots/${SPEC_ID}/implementations/${LANGUAGE}"

Expand Down Expand Up @@ -445,6 +463,7 @@ jobs:
lib_ver = '$LIBRARY_VERSION'
has_html = '$HAS_HTML' == 'true'
metadata_file = '$METADATA_FILE'
model = '$MODEL'
base_url = f'https://storage.googleapis.com/anyplot-images/plots/{spec}/{language}/{lib}'

# Preserve the original 'created' timestamp on regenerations.
Expand All @@ -466,11 +485,11 @@ jobs:
'specification_id': spec,
'created': created_ts,
'updated': ts,
# Reflects what claude_args=`--model sonnet` actually runs: whatever
# Claude Code's current "sonnet" alias resolves to.
# Reflects what claude_args=`--model {model}` actually runs: whatever
# Claude Code's current alias for the chosen model resolves to.
# Use the family name instead of a frozen version string so the
# metadata doesn't go stale every model release.
'generated_by': 'claude-sonnet',
'generated_by': f'claude-{model}',
'workflow_run': run_id,
'issue': issue,
'python_version': py_ver,
Expand Down Expand Up @@ -710,12 +729,14 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
MODEL: ${{ steps.inputs.outputs.model }}
run: |
# Use repository_dispatch as workaround for workflow_dispatch caching issue
gh api repos/${{ github.repository }}/dispatches \
-f event_type=review-pr \
-f 'client_payload[pr_number]='"$PR_NUMBER"
echo "::notice::Triggered impl-review.yml via repository_dispatch for PR #$PR_NUMBER"
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[model]=$MODEL"
echo "::notice::Triggered impl-review.yml via repository_dispatch for PR #$PR_NUMBER (model=$MODEL)"

- name: Determine result
id: result
Expand All @@ -736,6 +757,7 @@ jobs:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LIBRARY: ${{ steps.inputs.outputs.library }}
ISSUE: ${{ steps.issue.outputs.number }}
MODEL: ${{ steps.inputs.outputs.model }}
run: |
echo "::notice::Handling generation failure for $LIBRARY/$SPEC_ID"

Expand Down Expand Up @@ -775,7 +797,7 @@ jobs:

To retry manually:
\`\`\`
gh workflow run impl-generate.yml -f specification_id=${SPEC_ID} -f library=${LIBRARY} -f issue_number=${ISSUE}
gh workflow run impl-generate.yml -f specification_id=${SPEC_ID} -f library=${LIBRARY} -f issue_number=${ISSUE} -f model=${MODEL}
\`\`\`

---
Expand All @@ -798,7 +820,8 @@ jobs:
gh workflow run impl-generate.yml \
-f specification_id="${SPEC_ID}" \
-f library="${LIBRARY}" \
-f issue_number="${ISSUE}"
-f issue_number="${ISSUE}" \
-f model="${MODEL}"

echo "::notice::Triggered automatic retry for ${LIBRARY}/${SPEC_ID} (attempt $((ATTEMPT + 1)))"
echo "::notice::Triggered automatic retry for ${LIBRARY}/${SPEC_ID} (attempt $((ATTEMPT + 1)), model=${MODEL})"
fi
25 changes: 19 additions & 6 deletions .github/workflows/impl-repair.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ on:
description: "Current attempt number (1, 2, 3, or 4)"
required: true
type: string
model:
description: "Claude model to use (also threaded back into review)"
required: false
type: choice
default: 'sonnet'
options:
- haiku
- sonnet
- opus

# Per-library deps now come from `pyproject.toml` `lib-{library}` extras
# (single source of truth — same change as impl-generate.yml).
Expand Down Expand Up @@ -107,7 +116,7 @@ jobs:
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--model sonnet"
claude_args: "--model ${{ inputs.model || 'sonnet' }}"
allowed_bots: '*'
prompt: |
Read `prompts/workflow-prompts/impl-repair-claude.md` and follow those instructions.
Expand All @@ -126,7 +135,7 @@ jobs:
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--model sonnet"
claude_args: "--model ${{ inputs.model || 'sonnet' }}"
allowed_bots: '*'
prompt: |
Read `prompts/workflow-prompts/impl-repair-claude.md` and follow those instructions.
Expand Down Expand Up @@ -225,12 +234,14 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUM: ${{ inputs.pr_number }}
MODEL: ${{ inputs.model || 'sonnet' }}
run: |
# Use repository_dispatch as workaround for workflow_dispatch caching issue
gh api repos/${{ github.repository }}/dispatches \
-f event_type=review-pr \
-f "client_payload[pr_number]=$PR_NUM"
echo "::notice::Triggered impl-review.yml via repository_dispatch for PR #$PR_NUM"
-f "client_payload[pr_number]=$PR_NUM" \
-f "client_payload[model]=$MODEL"
echo "::notice::Triggered impl-review.yml via repository_dispatch for PR #$PR_NUM (model=$MODEL)"

# ========================================================================
# Failure handling: when the repair workflow itself crashes (e.g. Claude
Expand All @@ -245,6 +256,7 @@ jobs:
SPEC_ID: ${{ inputs.specification_id }}
LIBRARY: ${{ inputs.library }}
ATTEMPT: ${{ inputs.attempt }}
MODEL: ${{ inputs.model || 'sonnet' }}
run: |
# Restore ai-rejected label that was removed at start of repair so the
# PR state stays consistent (otherwise it looks "stuck approved").
Expand All @@ -264,7 +276,7 @@ jobs:

**Manual restart:**
\`\`\`
gh workflow run impl-repair.yml -f pr_number=${PR_NUM} -f specification_id=${SPEC_ID} -f library=${LIBRARY} -f attempt=${ATTEMPT}
gh workflow run impl-repair.yml -f pr_number=${PR_NUM} -f specification_id=${SPEC_ID} -f library=${LIBRARY} -f attempt=${ATTEMPT} -f model=${MODEL}
\`\`\`

---
Expand All @@ -283,5 +295,6 @@ jobs:
-f pr_number="$PR_NUM" \
-f specification_id="$SPEC_ID" \
-f library="$LIBRARY" \
-f attempt="$ATTEMPT"
-f attempt="$ATTEMPT" \
-f model="$MODEL"
fi
Loading
Loading