diff --git a/.claude/skills/merge-when-green/SKILL.md b/.claude/skills/merge-when-green/SKILL.md new file mode 100644 index 0000000..49426c7 --- /dev/null +++ b/.claude/skills/merge-when-green/SKILL.md @@ -0,0 +1,38 @@ +--- +name: merge-when-green +description: Merge a pull request once its CI has passed. This is the default for every pull request opened in this repo (a plain "pr" means open it and merge it when green); also use when the user says "merge" or runs /merge-when-green. +argument-hint: "[pull request number; default: the open pull request for the current branch]" +--- + +# Merge when green + +The user has made this the default for this repo: after opening a pull request for them ("pr"), follow this skill and merge it with a merge commit (`merge_method: "merge"`, not squash or rebase) as soon as CI is green, without asking again. Invoking it by hand, or saying "merge", covers any open pull request they name. The authorisation is for pull requests I opened or they named; it does not cover Dependabot or anyone else's pull requests unless they ask. If the user says "pr only" or "don't merge", open it and stop. + +## 1. Find the pull request +- With an argument, that number. Without one, the open pull request whose head is the current branch (`mcp__github__list_pull_requests`, state `open`, match `head.ref`). None, or more than one: say so and stop. +- Repository: `robdevops/ecobot` (check `git remote get-url origin` if in doubt). Read the pull request with `mcp__github__pull_request_read`. If it is already merged or closed, say so and stop. + +## 2. Is it green? +Read the check runs on the **current head commit** (`get_check_runs`), not an earlier one. +- **Green** = the `test` check is `success`, and no other check is `failure`, `cancelled` or `timed_out`. `skipped` is fine (`newest` and `live-evals` only run on the schedule and the Run workflow button, never on pull requests). +- **Pending** (`queued` or `in_progress`, or no checks yet because a push just landed): do not poll in a loop and do not `sleep`. Call `mcp__github__subscribe_pr_activity` for the pull request and end your turn saying you will merge when CI finishes; if a CI result arrives, start again at step 2. (Also schedule a check-in with `mcp__Claude_Code_Remote__send_later` about 10 minutes out if it is available, since events can be missed.) +- **Red**: do not merge. Read the failing job (`get_job_logs`), say what failed. If it is a pull request you opened and the fix is yours to make, fix and push; otherwise report and stop. Never skip, disable or loosen a test to get green, and never push an empty commit to re-run CI. + +## 3. Is it mergeable? +- `mergeable_state` of `dirty` (merge conflict): merge the base branch in and resolve it if the pull request is yours; for a Dependabot pull request comment `@dependabot rebase` and wait for its new CI run (back to step 2). Never rewrite someone else's branch. +- Review state: stop if a review has requested changes or a thread is open that names a red-circle (blocking) finding. Say what is open. +- Base moved since CI ran and it was not rebased: fine for GitHub's own check; the `test` run on the head commit is what counts. + +## 4. Merge +`mcp__github__merge_pull_request` with `merge_method: "merge"`. Then confirm it merged (`state: closed`, `merged: true`) and report the merge commit. + +## 5. After merging +- Bring the dev branch (the head branch, any branch but `main`) up to the merge: `git fetch origin main && git merge --ff-only origin/main && git push origin HEAD`. Never reset or recreate it; it persists between iterations. Delete no branch. +- If the user has other open pull requests that touch the same files (typically several Dependabot ones edit `.github/workflows/ci.yml`), list them: each may now be conflicted and need `@dependabot rebase` and a fresh CI run before its own merge. +- Unsubscribe from the pull request (`mcp__github__unsubscribe_pr_activity`). + +## Rules +- One pull request per invocation. "Merge them all" is a separate instruction, and each is still checked as above. +- Never force-merge past a failing or missing required check, never `force` anything, and never merge a pull request you only watched unless the user named it. +- Report outcomes plainly: what was green, what was skipped, what was merged, and anything left open. +- Keep the final message to a couple of words ("Merged #32.", "CI red: test."). Say nothing about each notification along the way. diff --git a/.claude/skills/ship/SKILL.md b/.claude/skills/ship/SKILL.md deleted file mode 100644 index e58b4d4..0000000 --- a/.claude/skills/ship/SKILL.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: ship -description: Start the pipeline that merges the dev branch into main (tests, pull request, merge). Use when the user says "ship", "pr", "merge" or runs /ship. ---- - -# Ship - -"ship" (or "pr", or "merge": all the same) starts the **Ship** workflow (`.github/workflows/ship.yml`) on the dev branch (the branch the session is on: any branch except `main`). The workflow runs the CI gate, opens the pull request to `main`, merges it with a merge commit, and fast-forwards the dev branch to `main`. Nothing here waits or merges by hand. - -1. Everything must be committed and pushed to the dev branch; push first if not. Run `python -m pytest -q` and `python scripts/eval_prompts.py` before pushing. -2. Start it: `mcp__github__actions_run_trigger` (`run_workflow`, owner `robdevops`, repo `ecobot`, workflow `ship.yml`, ref the dev branch). -3. Reply with one line: "Shipping." and the run link if known. Do not subscribe, poll or recap. GitHub tells the user if it fails; if they ask, read the run with `mcp__github__actions_get` and `get_job_logs`. -4. Do not reset or recreate the dev branch afterwards: the workflow fast-forwards it. Before the next change, `git fetch origin && git merge --ff-only origin/` if the remote moved. - -If the workflow cannot start (it is not on `main` yet, so the Run workflow button and the API do not know it), say so: the first ship has to be a pull request merged by hand. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ded211e..7921fa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - workflow_call: # the Ship workflow runs the same gate on the dev branch schedule: - cron: "17 21 * * 0" # Sundays 21:17 UTC: the check against the newest matplotlib, numpy and Pillow (the "newest" job below) workflow_dispatch: diff --git a/.github/workflows/ship.yml b/.github/workflows/ship.yml deleted file mode 100644 index 2c2fa47..0000000 --- a/.github/workflows/ship.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Ship - -# The one button: Actions > Ship > Run workflow, with the dev branch (any branch but main) selected. It runs the CI gate on that -# branch, opens the pull request to main (or reuses the open one), merges it with a merge commit, and fast-forwards the dev branch to -# the new main so the same branch carries on. Claude starts it too when asked to "ship". - -on: - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -concurrency: - group: ship - cancel-in-progress: false - -jobs: - test: - uses: ./.github/workflows/ci.yml - secrets: inherit - - merge: - needs: test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: Open the pull request, merge it, bring the dev branch up to main - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - branch="${GITHUB_REF_NAME}" - if [ "$branch" = main ]; then echo "Select the dev branch (not main) in 'Use workflow from'."; exit 1; fi - git fetch origin main - if [ -z "$(git log origin/main..HEAD --oneline)" ]; then echo "Nothing to ship: $branch has no commits main lacks."; exit 0; fi - if ! gh pr view "$branch" --json number >/dev/null 2>&1; then - gh pr create --base main --head "$branch" \ - --title "$(git log -1 --format=%s)" \ - --body "$(git log origin/main..HEAD --reverse --format='- %s')" - fi - gh pr merge "$branch" --merge - git fetch origin main - git push origin "origin/main:refs/heads/${branch}" || echo "::warning::$branch moved while shipping; merge main into it by hand." diff --git a/CLAUDE.md b/CLAUDE.md index 72a014d..0e59ee8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ # Working in this repo - **Iterating:** the "dev branch" is whichever branch the session is on, any branch except `main`. Keep using it between iterations: commit and push each change to it, with no pull request, so the user can pull it into their test environment. Never reset or recreate it. -- **Shipping:** "ship" (also "pr" or "merge": all the same) means start the Ship workflow on the dev branch and stop (see the `ship` skill). It tests, opens the pull request, merges it, and fast-forwards the branch to `main`. Do not open or merge the pull request by hand, and do not subscribe or wait. Other people's and Dependabot's pull requests are merged only when asked. +- **Pull requests:** "pr" (or "merge") means: create the pull request to `main`, subscribe to its events so CI shows up here (no polling), merge it with a merge commit once CI is green, then fast-forward the dev branch to `main` (`git fetch origin main && git merge --ff-only origin/main && git push`) and unsubscribe. Do all of it without asking. Finish with a couple of words, e.g. "Merged #32." or "CI red: ." Do not narrate notifications or recap. See the `merge-when-green` skill. "pr only" or "don't merge" means open it and stop. Other people's and Dependabot's pull requests are merged only when asked. - **Checks before pushing:** `python -m pytest -q` and `python scripts/eval_prompts.py` (see `DEVELOPMENT.md`). diff --git a/lib/charts.py b/lib/charts.py index 9d09bb0..e6eae07 100644 --- a/lib/charts.py +++ b/lib/charts.py @@ -85,8 +85,8 @@ class Look: in_headline: bool # the panel's name and unit are the figure's headline, and its colour key sits beside it -BIG = Look(30, 1.5, 8.5, 18, 1.2, 9, (0.35, 0.8), 6, 17, 0.26, 8.5, 1, True, 5, False, True) -SMALL = Look(20, 1.2, 7.5, 12, 0.8, 5, (0.25, 0.6), 4, 14, 0.3, 7, 2, False, 4, True, False) +BIG = Look(30, 1.5, 8.5, 18, 1.2, 9, (0.35, 0.8), 6, 17, 0.26, 10, 1, True, 5, False, True) +SMALL = Look(20, 1.2, 7.5, 12, 0.8, 5, (0.25, 0.6), 4, 14, 0.3, 8.5, 2, False, 4, True, False) def _look_for(chart: Chart) -> Look: @@ -381,7 +381,7 @@ def _render_rose(fig, compass: Compass): a, b = compass.speed_steps fig.legend([Line2D([], [], marker="s", linestyle="", markersize=6, color=c) for c in WIND_STEPS], [f"under {a}", f"{a}–{b}", f"{b}+ km/h"], loc="lower right", bbox_to_anchor=(0.985, 0.0), ncol=3, - frameon=False, fontsize=7, labelcolor=MUTED, handletextpad=0.2, columnspacing=0.9) + frameon=False, fontsize=8.5, labelcolor=MUTED, handletextpad=0.2, columnspacing=0.9) # ---------- several panels on one time axis ---------- @@ -461,14 +461,14 @@ def _end_labels(ax, drawn: list[tuple], show_floor: bool = False): if not show_floor and max(d[2][-1] for d in drawn) - y_lo < 0.05 * (y_hi - y_lo): return height_pt = ax.get_position().height * ax.figure.get_figheight() * 72 - gap = 8 * (y_hi - y_lo) / height_pt # 8 points, in data units + gap = 10 * (y_hi - y_lo) / height_pt # 10 points, in data units placed = [] for colour, xs, ys in sorted(drawn, key=lambda d: d[2][-1]): y = max(ys[-1], placed[-1] + gap) if placed else ys[-1] placed.append(y) shade = colour ax.annotate(_figure(ys[-1]), (xs[-1], ys[-1]), xytext=(1.014, y), textcoords=("axes fraction", "data"), va="center", ha="left", - fontsize=7, fontweight="bold", color=shade, annotation_clip=False, + fontsize=8.5, fontweight="bold", color=shade, annotation_clip=False, arrowprops={"arrowstyle": "-", "color": shade, "linewidth": 0.6, "alpha": 0.6, "shrinkA": 0, "shrinkB": 2}, bbox={"boxstyle": "round,pad=0.15", "fc": "none", "ec": "none"}) @@ -485,7 +485,7 @@ def _draw_panel(ax, p: Panel, tz: tzinfo, first: int, x0: float, x1: float, look base += np.asarray(column, dtype=float) ax.set_ylim(0, 100) ax.legend([Line2D([], [], marker="s", linestyle="", markersize=5, color=c) for c in ZONE_COLOURS], - ["good", "poor", "very poor"], loc="lower right", bbox_to_anchor=(1.0, 1.0), frameon=False, fontsize=7, + ["good", "poor", "very poor"], loc="lower right", bbox_to_anchor=(1.0, 1.0), frameon=False, fontsize=8.5, labelcolor=TEXT, ncol=3, handletextpad=0.2, columnspacing=0.9, borderaxespad=0.1) return 0, _end_of(bx, s.width, x1), [] if not p.lines: # rain on its own diff --git a/tests/charts/air.png b/tests/charts/air.png index f8b87ef..a6072bb 100644 Binary files a/tests/charts/air.png and b/tests/charts/air.png differ diff --git a/tests/charts/all_week.png b/tests/charts/all_week.png index ea2a7d8..589979f 100644 Binary files a/tests/charts/all_week.png and b/tests/charts/all_week.png differ diff --git a/tests/charts/humidity_rain.png b/tests/charts/humidity_rain.png index 10fd668..d83024f 100644 Binary files a/tests/charts/humidity_rain.png and b/tests/charts/humidity_rain.png differ diff --git a/tests/charts/quarter.png b/tests/charts/quarter.png index 0039b53..4078747 100644 Binary files a/tests/charts/quarter.png and b/tests/charts/quarter.png differ diff --git a/tests/charts/temperature.png b/tests/charts/temperature.png index ed70f43..d7fe352 100644 Binary files a/tests/charts/temperature.png and b/tests/charts/temperature.png differ diff --git a/tests/charts/wind.png b/tests/charts/wind.png index 9da8a96..f4c1e64 100644 Binary files a/tests/charts/wind.png and b/tests/charts/wind.png differ