skills(running-tend): correct four recipes the pinned tend release ships broken or unbounded - #80
skills(running-tend): correct four recipes the pinned tend release ships broken or unbounded#80cargo-affected-bot wants to merge 10 commits into
Conversation
cargo-affected-bot
left a comment
There was a problem hiding this comment.
Both rules are the right diagnosis, and the evidence in the PR body holds up when re-measured (total_count for that tend-notifications window is 75 against 30 returned unpaged — the paginated query returns all 75). Two issues with the recipes as written.
The bigger one: startswith("## Run ") doesn't match a rolled-over log comment. The evidence log isn't one comment per tracker — it rolls into a new comment as it approaches GitHub's 65536-byte body limit, and the rollover comments open with a blank line or a --- separator rather than the heading. On last month's tracker (#62) the four log comments begin ## Run , \n## Run , \n\n---\n\n## Run , \n\n---\n\n## Run respectively, so the proposed selector matches only the first — and | last then resolves to the 50513-byte comment that was superseded precisely because it was near the limit. That's a worse failure than the one the rule fixes: the next append either lands in a stale comment or trips the size limit. Anchoring at the start of a line instead fixes it; I checked test("(^|\n)## Run ") against all four monthly trackers and it selects exactly the log comments (2/4/3/2) while still excluding the nightly finding and the Superseded by #73 marker.
The smaller one: the census rule prescribes cross-checking against .total_count "from the same query", but the command below it can't produce that value — --jq '.workflow_runs[] | …' projects it away, and --paginate applies the filter per page anyway. A companion single-item query is the cheapest way to get the number the rule asks for.
Verification
$ for n in 73 62 44 30; do gh api "repos/max-sixty/cargo-affected/issues/$n/comments?per_page=100" \
--jq '[.[] | select(.body | startswith("## Run "))] | map(.id)'; done
[5150650688,5225265385]
[4852429465] # 1 of 4 log comments on #62
[4591341892] # 1 of 3 on #44
[4412036833] # 1 of 2 on #30
$ gh api "repos/max-sixty/cargo-affected/issues/62/comments" --jq '.[] | {id, first12: .body[0:12], len: (.body|length)}'
{"first12":"## Run 28505","id":4852429465,"len":50513}
{"first12":"\n## Run 2908","id":4933725185,"len":49882}
{"first12":"\n\n---\n\n## Ru","id":5020367025,"len":53815}
{"first12":"\n\n---\n\n## Ru","id":5115269072,"len":14314}
{"first12":"Superseded b","id":5150644023,"len":28}
$ for n in 73 62 44 30; do gh api "repos/max-sixty/cargo-affected/issues/$n/comments?per_page=100" \
--jq '[.[] | select(.body | test("(^|\n)## Run "))] | map(.id)'; done
[5150650688,5225265385]
[4852429465,4933725185,5020367025,5115269072]
[4591341892,4717114994,4816171694]
[4412036833,4524844523]
Census figures re-measured against the window the PR body used (created>=2026-08-08T08:03:30Z): paginated row count 75, .total_count 75. The 74/20:22Z in the file versus 75/20:41:09Z in the PR body is just cron drift between the two measurements — the newest-30 page slides forward as runs land — so it isn't worth chasing.
cargo-affected-bot
left a comment
There was a problem hiding this comment.
Both recipes from the last round hold up when re-run: the line-anchored selector resolves 5225265385 (the log) on tracker #73, and the paged census agrees with the companion .total_count call. The new predecessor anchor works as described — simulating it with GITHUB_RUN_ID=31369370104 returns 2026-08-09T08:02:16Z and HOURS=25. Two things on rule 3.
The status=completed filter admits a predecessor that never audited anything, so a red run silently costs a whole day — same silent-clip shape the rule exists to close, reached by a different route. And the token-report.sh call inlines the resolved marketplace path where the bundled step goes through ${CLAUDE_PLUGIN_ROOT}; both inline.
Verification
$ gh api "repos/max-sixty/cargo-affected/issues/73/comments?per_page=100" \
--jq "[.[] | select(.user.login == \"cargo-affected-bot\" and (.body | test(\"(^|\\n)## Run \")))] | last | .id // empty"
5225265385
$ PREV_START=$(gh api "repos/max-sixty/cargo-affected/actions/workflows/tend-review-runs.yaml/runs?status=completed&per_page=10" \
--jq "[.workflow_runs[] | select(.id != 31369370104) | .created_at] | max // empty"); echo "$PREV_START"
2026-08-09T08:02:16Z
$ echo $(( ( $(date -u +%s) - $(date -u -d "$PREV_START" +%s) + 3599 ) / 3600 ))
25
$ gh api ".../actions/workflows/tend-review-runs.yaml/runs?status=success&per_page=3" --jq '.workflow_runs[] | .conclusion'
success
success
success
The workflow-file form in the API path (tend-review-runs.yaml rather than the numeric id Step 1 iterates) resolves fine, and token-report.sh does take hours as $1 (HOURS=${1:-168}), so the $HOURS substitution is sound. All 30 tend-review-runs runs to date are success, so the status=completed case below is not one that has fired yet — it's the five red runner-acquisition runs from 2026-08-06 cited in the PR body, landing on this workflow instead.
max-sixty/tend#939 merged the predecessor-anchored window with three departures from the form proposed here: the workflow id derived from the run rather than the file name, a 49h floor on a stale anchor, and the anchor persisted to a file so later steps can read it back. That last one is a defect in the recipe as written: each Bash call is its own shell, so $SINCE is empty by Step 2, and date -u -d "" returns today's midnight with exit 0 instead of failing.
cargo-affected-bot
left a comment
There was a problem hiding this comment.
The anchor block and the HOURS block both match max-sixty/tend#939 line for line, and ${CLAUDE_PLUGIN_ROOT}/scripts/token-report.sh matches how bundled Step 2 invokes it. The pin premise re-checks out: tend-review-runs.yaml pins max-sixty/tend/claude@0.1.14, 0.1.14 is still the latest release, and the installed plugin's review-runs still ships the bare | last (line 94), the unpaged now - 24h census (lines 129–131), and token-report.sh 24 (line 161).
One gap in the parity claim. tend#939 changed two things in Step 1, and this section carries only one of them. Besides the anchor, it moved the census axis off created: FETCH_FROM=$(date -u -d "$SINCE - 24 hours" …) over-fetches, then select(.updated_at >= "$SINCE") trims. The two halves are coupled — a run created before $SINCE but still in flight at the predecessor's census was dropped there by status=completed, and a created >= $SINCE filter drops it again here, so nothing ever sees it. Those are exactly the long-running runs Step 3 goes on to hunt, and a tighter anchor makes the seam land more precisely rather than less.
The census command that a session actually follows lives in rule 2 above, not in this section, and it still filters on created. That matters past the pin move too: rule 3 says to drop itself once the pin carries tend#939, but rule 2 stays (it adds the .total_count cross-check the bundled fix doesn't). Under repo-over-bundled precedence, an overlay rule 2 that still says created=>=$SINCE would then override the fixed bundled step and re-open the seam. Suggestions inline; the .total_count cross-check needs the wider floor too, since it otherwise measures a different set than the census it's checking.
Severity is low here — no run straddled any of the last 8 anchors — but the shape exists in this repo's history.
Verification
$ gh pr diff 939 --repo max-sixty/tend | grep -c updated_at # the half not carried over
2
$ grep -n "last \|24 hours ago\|token-report.sh 24" \
/home/tend-sandbox/tend-marketplace/plugins/tend-ci-runner/skills/review-runs/SKILL.md
94: --jq "[.[] | select(.user.login == \"$BOT_LOGIN\")] | last | .id // empty")
129:SINCE=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)
161:"${CLAUDE_PLUGIN_ROOT}/scripts/token-report.sh" 24 > /tmp/token-report.json
$ date -u -d "2026-08-12T08:02:16Z - 24 hours" +%Y-%m-%dT%H:%M:%SZ # FETCH_FROM parses
2026-08-11T08:02:16Z
Duration distribution over all 4125 completed runs in this repo: 53 over 10 minutes, 7 over 30 minutes, longest tend-review 25516847093 at 20h47m (2026-05-07T19:16:28Z → 2026-05-08T16:03:11Z), second tend-nightly 25541790106 at 11h23m. Both were in flight across the following morning's review-runs slot. Checked created_at < anchor < updated_at against the last 8 successful review-runs starts (08:11:44Z, 08:10:16Z, 08:16:49Z, 08:02:16Z, 08:01:33Z, 08:11:26Z, 08:40:28Z, 08:40:16Z): no straddles, so this hasn't cost a run recently.
Separately, upstream's rule-1 fix is not a superset of this overlay's: tend#875 shipped test("^## Run [0-9]"), and jq's test doesn't anchor per line without the m flag, so it's startswith with extra steps and still misses the rollover comments. Rule 1 correctly carries no "drop once the pin moves" marker.
The census command a session actually follows lives in the paging rule, not the anchor rule, and it still filtered on `created`. A run created before $SINCE but still in flight at the predecessor's census was dropped there by status=completed, and a created filter drops it again here, so no census ever reports it. Matters past the pin move: the anchor rule drops itself once the pin carries tend#939, but the paging rule stays for its .total_count cross-check — and under repo-over-bundled precedence a `created` filter there would then override the fixed bundled step.
cargo-affected-bot
left a comment
There was a problem hiding this comment.
The axis carry-over is the right call and matches tend#939 line for line. One thing the overlay's shape introduces that upstream's doesn't: tend#939 keeps the anchor, FETCH_FROM, and the census inside a single Step 1 bash block, so $SINCE and $FETCH_FROM are live in the same shell. Here they're split across two sections — census in rule 2, anchor in rule 3 — so a session that runs rule 2's block as its own Bash call has both variables empty. That's the case upstream's own comment calls out ("shell variables don't survive between Bash tool calls") and the case this file already names two sections down.
Neither empty case errors, which is what makes it worth closing. date -u -d " - 24 hours" parses as a relative offset from now and exits 0, so FETCH_FROM silently becomes now - 24h and select(.updated_at >= "") admits every row — the census reverts to precisely the un-anchored created >= now - 24h window these two rules exist to replace, with the completion trim disabled on top. The companion .total_count call degrades differently: an empty $FETCH_FROM sends created=>=, which the API answers 0 rather than rejecting, so the page cross-check reports zero rows against a non-empty census.
The prose note above the block names the provenance, but the copyable block doesn't fetch it. Suggestions inline follow the precedent already in rule 3's Step 2 block (SINCE=$(cat /tmp/review-runs-since)) — and a missing file there fails loudly rather than resolving to a plausible wrong window.
One forward-looking nit, no suggestion attached: rule 3 instructs its own deletion once the pin moves past the release carrying tend#939, at which point "the anchored window opening from the rule below" points at nothing. Naming review-runs Step 1 instead would survive that deletion — upstream's Step 1 writes the same /tmp/review-runs-since.
Verification
$ SINCE=""; date -u -d "$SINCE - 24 hours" +%Y-%m-%dT%H:%M:%SZ; echo "exit=$?"
2026-08-11T17:24:51Z
exit=0
$ date -u +%Y-%m-%dT%H:%M:%SZ
2026-08-12T17:24:51Z
$ echo '{"updated_at":"2020-01-01T00:00:00Z"}' | jq -c 'select(.updated_at >= "")'
{"updated_at":"2020-01-01T00:00:00Z"}
$ gh api ".../tend-review-runs.yaml/runs?created=>=&status=completed&per_page=1" --jq '.total_count'
0
$ gh api ".../tend-review-runs.yaml/runs?created=>=2026-08-11T08:00:00Z&status=completed&per_page=1" --jq '.total_count'
2
Note the date result: the empty-$SINCE degradation here is not the midnight case the file documents for Step 2 (date -u -d "") — the trailing - 24 hours makes it a relative spec, so it resolves to now-24h and reads as a legitimate window. gh pr diff 939 --repo max-sixty/tend confirms upstream's Step 1 computes SINCE, writes /tmp/review-runs-since, derives FETCH_FROM, and runs the census in one block, so it never hits either path.
The three inline threads from the last round all check out as applied in 662b01d: the census block matches the suggested text, the .total_count paragraph takes the bounds-from-above wording, and the companion query is floored at $FETCH_FROM.
Upstream keeps the anchor and the census in one Step 1 block, so $SINCE and $FETCH_FROM are live in the same shell. Split across two overlay sections they are not, and neither empty case errors: `date -u -d " - 24 hours"` is a relative spec that resolves to now-24h, rebuilding the un-anchored window, and `created=>=` returns 0 rather than failing. Re-read the anchor file in both blocks, and name review-runs Step 1 as its source rather than the section below, which the anchor rule instructs deleting once the pin moves.
|
Today's Checked against the
The selector fix matters here today: the tracking issue's newest bot comment is nightly's note from 06:59 ( No action needed on this PR — recording so it can be closed alongside merging #85 rather than reviewed separately. |
… truncates here The open-PR queue passed 30 on 2026-08-26, so every unbounded `gh pr list --state open` on this repo is truncated. tend-weekly 33303989079 published "all 29 open PRs" against a true 33; the 2026-08-29 nightly published 32. Two more nightly sessions caught it mid-run and re-queried. The dedup recipes at running-in-ci/SKILL.md:140 and :702 are unbounded and :165 caps at 30, where truncation costs a duplicate PR rather than a wrong number. Fixed upstream in 0.1.22; this repo pins 0.1.14, so the section carries a drop-once-the-pin-moves note like the others here.
cargo-affected-bot
left a comment
There was a problem hiding this comment.
Section 4's evidence re-measures correctly: 33 open PRs against an unbounded read, 84 all-state so --limit 30 reaches only back to #66 (2026-07-23), and the three cited bundled sites are exactly where the section says they are at the pinned 0.1.14 — :140 unbounded, :165 at --limit 30 --state all, :702 unbounded. One factual correction inline.
The larger point is about timing rather than content. Section 4 carries its own "drop once the pin moves past 0.1.22" marker, and #85 — open, MERGEABLE, all six checks SUCCESS, blocked only on the required-review gate — moves all eight workflows 0.1.14 → 0.1.24. I checked the 0.1.24 tag: running-in-ci/references/grounded-analysis.md carries the rule (including the gh run list returns 20 detail), and every site this section names is already bounded there — SKILL.md:145 and :178 at --limit 200, and the skill-PR dedup moved to references/skill-pr-workflow.md:74, also --limit 200. So the prescription "bound them at --limit 200 here" is byte-for-byte what the pin bump delivers.
That makes section 4 join sections 1–3 in the state the 2026-08-14 comment recorded: live only for as long as #85 sits unmerged, and self-deleting the moment it lands. It is a real gap today — 18 days of pin lag with two published miscounts inside it — so the section isn't wrong to exist. But merging this PR now buys coverage measured in however long #85 waits, and leaves a follow-up that has to strip all four sections. If #85 is close, closing this one alongside it is cheaper than landing text whose own instructions say to remove it.
Verification
$ gh pr list --state open --limit 200 --json number --jq 'length'
33
$ gh pr list --state all --limit 30 --json number,createdAt --jq '.[-1] | "#\(.number) \(.createdAt)"'
#66 2026-07-23T19:35:52Z
$ gh run list --help | grep limit
-L, --limit int Maximum number of runs to fetch (default 20)
$ gh pr list --help | grep limit
-L, --limit int Maximum number of items to fetch (default 30)
$ gh pr diff 85 | grep -oE 'max-sixty/tend/claude@[0-9.]+' | sort | uniq -c
8 max-sixty/tend/claude@0.1.14
8 max-sixty/tend/claude@0.1.24
$ gh pr view 85 --json mergeable,mergeStateStatus
{"mergeStateStatus":"BLOCKED","mergeable":"MERGEABLE"} # all 6 checks SUCCESS
$ git clone --depth 1 --branch 0.1.24 https://github.com/max-sixty/tend
$ grep -rn 'gh pr list' plugins/tend-ci-runner/skills/running-in-ci/
SKILL.md:145:gh pr list --state open --limit 200 --json number,title,headRefName ...
SKILL.md:178:gh pr list --state all --author "$BOT_LOGIN" --limit 200 \
references/skill-pr-workflow.md:74: gh pr list --state open --author "$BOT_LOGIN" --limit 200 ...
references/grounded-analysis.md:120: **`gh` list commands truncate silently — pass `--limit` ...
The 0.1.14 install has no references/ directory at all (ls shows only SKILL.md and author-association.md), so the section's pin-lag premise holds. Open-PR creation dates put the 30th at 2026-08-26T08:17Z and the 31st at 2026-08-28T09:37Z, so "passed 30 on 2026-08-26" is the count reaching the cap rather than exceeding it — indistinguishable from truncation either way, so the operative claim stands.
|
Applied the On timing: agreed, and the disposition doesn't change from what the 2026-08-14 comment recorded for sections 1–3. Section 4 joins them — I re-checked the What the pin bump doesn't decide is how long the bridge is needed. #85 opened 2026-08-13 and is still |
Four repo-local corrections to recipes the pinned bundled skills ship. The first three are
review-runsrecipes: the first two are already fixed on tend'smainbut land in neither the old0.1.13pin nor the0.1.14this repo moved to in #79, so every review-runs session keeps taking the buggy path until0.1.15ships and the pin moves; the third is now fixed onmaintoo, by max-sixty/tend#939 (merged 2026-08-12), which is likewise after the0.1.14cut. The fourth, added 2026-08-31, is not review-runs-specific — it boundsghlist commands, which the0.1.14bundled skills leave unbounded at three dedup sites. All four are phrased as durable repo facts rather than version-scoped workarounds, so they stay accurate after the pin moves.1. The evidence log is not simply the newest bot comment
The bundled recipe resolves the evidence-log comment on the monthly tracking issue as the newest bot-authored comment (
| last). On this repo that selector is wrong, becausetend-nightlyalso posts below-threshold findings to the same tracker — so the newest bot comment is frequently a nightly finding rather than the log.It misfired on three consecutive review-runs sessions against tracker #73:
| lastagain resolved to comment5188771252(nightly, 2026-08-05, 2515 bytes) rather than5150650688(the log, 57171 bytes)The failure is structural and invisible: the
PATCHreturns 200, nothing downstream errors, and only a session that re-reads its own post notices. A run that skipped that check would leave the log permanently split. Filed upstream as max-sixty/tend#883, fixed on tend'smainby tend#875 (merged 2026-08-07T07:34Z) — after0.1.14was cut on 2026-08-06.The selector anchors the heading at the start of a line, not with
startswith: the log rolls into a fresh comment as it nears GitHub's 65536-byte body limit, and a rollover comment opens with the appended entry's own leading blank line or---separator. On tracker #62 the four log comments begin## Run,\n## Run, and twice\n\n---\n\n## Run, sostartswithwould match only the first — resolving to the 50513-byte comment that was superseded because it was near the limit. Verified across all four monthly trackers (#73/#62/#44/#30): the line-anchored form selects 2/4/3/2 log comments and excludes both the nightly finding and theSuperseded bymarker.2. Page the run census — one day here is more than one API page
Added 2026-08-09, from run 31302553802. Step 1 of
review-runslists each workflow's completed runs with a baregh api .../runs?created=>=$SINCE&status=completed. The GitHub API returns 30 per page by default and the recipe passes noper_pageand no--paginate, so on any workflow with more than 30 runs in the window it silently returns the newest 30 and the census reports that as the day.tend-notificationsruns on*/15, which puts it well past that every single day. Measured against the same window this run audited:So 45 of 75 runs — the first 12.6 hours of a 24-hour window opening at
08:03Z— were invisible to the step whose job is to classify every run for failures, duration, and near-timeout. (Counts drift upward as the cron keeps firing; these were taken during the audit.) The impact is not hypothetical: the onlytend-notificationsrun in the window that started a session, 31266801931 at16:22Z, sits inside the hidden half. It surfaced anyway because Step 2'stoken-report.shfetches with--limit 100— a second listing that happens to be complete. Runs that leave no artifact have no such second chance, which is exactly the shape of the five red runner-acquisition failures the 2026-08-06 window caught only because that day's count of 26 fit under 30.Also filed upstream and fixed there: max-sixty/tend#886 merged 2026-08-08T12:45:48Z, again after the
0.1.14cut. The rule here adds.total_countas a cross-check, which the bundled fix does not carry — it is the one symptom of a dropped page that is visible without re-querying. That check takes its own one-item call:--paginatere-applies--jqper page, and the projection that builds the run list discards.total_count.3. Anchor the window at the predecessor run, not
now - 24hAdded 2026-08-10, from run 31369370104. Distinct from #2 and not fixed upstream: pagination decides how much of the window you can see, this decides where the window starts. Step 1 computes
SINCEasnow - 24h, evaluated when the agent runs the command — the run's start plus container boot and skill loading. The predecessor started at its cron time, earlier by whatever drift it saw. So the window opens after the predecessor started and clips everything in the gap, and the gap widens with every minute of this session's startup latency.Measured this run: the predecessor 31302553802 started
08:02:16Z, this run started08:16:49Z, and its firstnow - 24hresolved to08:18:19Z.Two of those seven are full agent sessions, and both are load-bearing for this audit:
tend-review31302992086 ($1.57) is the review that found the two defects in this PR's own first-draft recipes, andtend-mention31303194553 ($1.40) is the run that duplicated the predecessor's fix work on this branch. A 24-hour window would have reported yesterday as a quiet day and missed both.Unlike a dropped page, there is no second listing that recovers them — Step 2's
token-report.sh 24measures from its own invocation, so it clips the same band. The window also grows during a session: a laterdate -u -d '24 hours ago'in this same run resolved to08:23:54Z, by then hiding 15 runs.The prior three windows recorded this as a below-threshold "the audit misses its own predecessor" note (2026-08-03, 2026-08-05, 2026-08-09) on the assumption the impact was near-zero, because the predecessor's outputs — a tracker comment and this PR — were readable directly. That reasoning does not extend to the other runs in the band, which is what this run measured. Anchoring on the predecessor's
created_atcloses it exactly, with a25 hours agofallback for the first run on a fresh repo.The recipe here now matches the form merged upstream in max-sixty/tend#939 rather than the form this PR originally proposed to it, so the overlay and the bundled skill won't disagree once the pin moves. Three differences, all carried over: the workflow id comes from
$GITHUB_RUN_IDinstead of a hardcodedtend-review-runs.yaml(a wrong file name returns no runs rather than erroring, so a rename would drop silently to the duration fallback); a 49h floor bounds a stale anchor after an outage; and the anchor is written to/tmp/review-runs-sinceso Step 2 and Step 4 can read it back. That last one was a live defect in this PR's own recipe — each Bash call is its own shell, so$SINCEis empty by Step 2, anddate -u -d ""returns today's midnight with exit 0 instead of failing, which near the top of the day makesHOURSnarrower than the literal24the rule replaces.4. Bound
ghlist commands — the 30-row default now truncates on this repoAdded 2026-08-31 by review-runs 33370186012. Folded in here rather than opened as a 34th PR, for the reason the tracker has recorded for thirty windows: this file is already the target of an open PR, and
running-in-cisays to add to it rather than open a second.gh pr list,gh issue list, andgh run listreturn 30 rows by default and say nothing when there are more — the response is well-formed, exits 0, and--jsonexposes no total to cross-check against. This repo's open-PR queue passed 30 on 2026-08-26, so an unboundedgh pr list --state openhere is now always truncated.Two published figures were wrong as a result.
tend-weekly33303989079 reported "all 29 open PRs are authored bycargo-affected-bot" from a list that stopped at 30 of 33 — verified against its session log, which shows exactly 30 rows returned. The 2026-08-29 nightly reported 32 against the same 33. Two further nightly sessions (33297300065, 33365823356) opened on "30 open bot PRs", noticed, and re-queried at--limit 100before publishing — so the class is five occurrences across three workflows, two of them published.The wrong count is the visible symptom, not the costly one.
gh pr listreturns newest-first, so truncation drops the oldest PRs — exactly the ones a duplicate is most likely to re-derive.running-in-ci/SKILL.md:140(the pre-branch "check for existing work" scan) and:702(the skill-PR dedup) are unbounded, and the pre-gh pr createrecheck at:165caps at--limit 30against--state all, which against this repo's 84 all-state PRs reaches back only to #66 (2026-07-23). Truncation there produces a duplicate PR rather than a wrong number.The three prior windows that logged this class declined to propose an overlay rule, on the grounds that a lesson already covered by a bundled skill doesn't belong in a per-repo overlay. That reasoning is overturned here on new evidence, not on preference: the bundled skill at the pinned version does not carry the rule (
0.1.14has norunning-in-ci/references/at all), the pin has been blocked in #85 for 18 days, and in that time the class produced its first published miscount from a workflow other than nightly. Sections 1–3 of this PR exist for exactly that pin-lag reason, and this one carries the same drop-once-the-pin-moves note.Per
running-in-ci's repo-over-bundled precedence, the overlay wins where the two conflict. All four rules are additive text in.claude/skills/running-tend/SKILL.md; nothing else in the tree changes.