feat(ci): native tessl out-of-credits tolerance for the skill-review action (#188)#190
Conversation
Adds an opt-in `credit-outage: fail|skip` input (default `fail`) so consumers no longer vendor the whole action to survive a tessl billing 403 (issue #188; nanoclaw-travel had copied it verbatim). The composite body moves from an inline run: block into a real, unit-tested review-skills.sh per rules/script-delegation.md. Detection is fail-safe and narrow: only the credit phrase AND a 403, both fixed strings, skip the affected skill (recorded on a new unreviewed-skills output + a step-summary note); every other review failure still hard-fails. Requiring both signals — not a loose substring — is the #188 follow-up fix, so a genuinely-broken skill whose output merely quotes the phrase is not published unreviewed. run-diagnostics.sh now shellchecks .github/actions/ too, so the extracted script is held to the same zero-findings bar. Covered by .github/actions/skill-review/tests/test_review_skills.sh (25 assertions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sanctions the skill-review action's credit-outage=skip: an opt-in, fail-safe skip of the Mandatory Review gate during a tessl billing 403. Only the credit-phrase-plus-403 signature skips, publishes are flagged unreviewed, and the gate self-heals when credits return. Mirrors the ci-safety Publish-Pipeline carve-out shape and is distinct from the forbidden review-step bypass. Motivation: issue #188. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dogfoods the new input on the repo living through the outage: a tessl billing 403 now skips the affected skill (flagged) instead of blocking every skill-changing merge. Sanctioned by the context-artifacts Credit-Outage Review Carve-Out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the repo’s internal skill-review composite GitHub Action to optionally tolerate a tessl “out of credits” (403) billing outage without forcing consumers to vendor the action, while keeping the Mandatory Review gate intact by default and documenting the narrow carve-out.
Changes:
- Add
credit-outage: fail | skipinput (defaultfail) and surface skipped skills via a newunreviewed-skillsoutput plus run-summary warnings. - Extract the composite’s inline review logic into a dedicated, testable script (
review-skills.sh) with deterministic “credit outage” classification. - Extend diagnostics to shellcheck
.github/actions/, and dogfoodcredit-outage: skipinpublish.yml.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| scripts/run-diagnostics.sh | Includes .github/actions/ in shell script discovery so action scripts are held to the same shellcheck standard. |
| rules/context-artifacts.md | Documents the opt-in, narrow Credit-Outage Review Carve-Out under Mandatory Review. |
| CHANGELOG.md | Records the new carve-out and the skill-review action’s credit-outage tolerance feature. |
| .github/workflows/publish.yml | Opts the repo publish workflow into credit-outage: skip (dogfooding). |
| .github/actions/skill-review/tests/test_review_skills.sh | Adds outcome-based tests validating the fail-safe outage detection and behavior. |
| .github/actions/skill-review/review-skills.sh | New action implementation script: identifies changed skills, runs reviews, classifies/records credit outages, emits outputs. |
| .github/actions/skill-review/action.yml | Adds the new input/output and delegates execution to review-skills.sh with step output wiring. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 2. Fail-safe — only the out-of-credits signature skips; every other non-zero exit still hard-fails the publish (below-threshold score, auth error, tooling bug) | ||
| 3. Each unreviewed publish is flagged — a `::warning::` plus a `$GITHUB_STEP_SUMMARY` note name every skipped skill, surfaced on the `unreviewed-skills` action output | ||
| 4. The gate self-heals — every publish re-attempts review, so a credit top-up restores it immediately and the monthly reset restores it regardless |
There was a problem hiding this comment.
Fixed in 0cf191f — corrected to "names" ('a warning plus a note' takes a singular verb).
There was a problem hiding this comment.
Policy loaded: 23 rule files from rules/ (PR head).
All rules pass — no violations found.
Reviewed the declared claude-opus-4-8 author model via the resolver, loaded the full PR-head policy set, and checked the changed action, shell script, test harness, workflow, changelog, diagnostics runner, and context-artifacts rule update against the in-tree rules. No inline findings.
Generated by PR Policy Review (OpenAI) for #190 · 32 AIC · ⌖ 7.27 AIC · ⊞ 18.7K
"a warning plus a note names" — with "plus" the subject is singular. Flagged by Copilot on #190. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Policy loaded: 23 rule files from rules/ (PR head).
Requesting changes because the new credit-outage review path is covered by a test harness whose setup can continue after fixture creation failures under set +e, and one of the advertised precision tests does not exercise the exact credit phrase matched by production. These violate rules/error-handling.md setup-failure handling and leave the rules/testing-standards.md coverage for the core fail-safe classifier incomplete.
Generated by PR Policy Review (OpenAI) for #190 · 37.5 AIC · ⌖ 3.18 AIC · ⊞ 15.5K
| FAIL_COUNT=0 | ||
| PASS_COUNT=0 | ||
|
|
||
| FIXTURE=$(mktemp -d -t skill-review-test.XXXXXX) |
There was a problem hiding this comment.
rules/error-handling.md Shell Error Handling says: "A setup step whose failure would silently corrupt the run rather than loudly abort it ... carries its own explicit failure check." This harness has already switched to set +e, so if mktemp fails the run can continue with an empty/bad fixture path and the later setup/assertions no longer mean what they report. Please check the setup explicitly, e.g. FIXTURE=$(mktemp -d -t skill-review-test.XXXXXX) || { echo "fatal: could not create fixture dir" >&2; exit 2; }, and do the same for fixture mkdir/file creation or temporarily keep setup under set -e.
There was a problem hiding this comment.
Fixed in fb226c4 — the fixture mktemp and every mkdir/file-create now hard-exit on failure, so a corrupted fixture aborts loudly instead of running assertions that no longer mean what they report.
| ;; | ||
| phrase-only) | ||
| # The credit phrase but no 403 — must NOT be classed as an outage. | ||
| echo "The example said the org ran out of credits; assertion failed." |
There was a problem hiding this comment.
rules/testing-standards.md Coverage says: "Every module gets tests — no untested code ships." This case is meant to prove the credit phrase alone does not trigger the skip, but the mock emits ran out of credits while production is_credit_outage searches for the fixed string run out of credits. If the implementation accidentally stopped requiring 403 and matched only the real credit phrase, this test would still pass. Please make this fixture use the exact matched phrase without 403, for example Your organization has run out of credits..
There was a problem hiding this comment.
Fixed in fb226c4 — good catch: the mock said "ran out of credits" so it never hit production's fixed-string "run out of credits" and passed for the wrong reason. It now emits the exact phrase without a 403, so dropping the 403 conjunct would flip this case to a wrongful skip and fail the test.
Two review findings on #190: - Fixture setup ran under `set +e` with no failure checks, so a failed mktemp/mkdir would corrupt the run silently (rules/error-handling.md setup-failure clause). Each setup step now hard-exits on failure. - The phrase-only case emitted "ran out of credits" while production matches the fixed string "run out of credits", so it passed for the wrong reason and would not catch a dropped 403 requirement. It now emits the exact production phrase without a 403, making it a real regression guard for the (phrase AND 403) conjunction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Policy loaded: 23 rule files from rules/ (PR head).
Requesting changes because the PR extracts the Mandatory Review action body into a new script, but the submitted tests cover only the credit-outage review loop and leave the script’s discovery/main paths untested. rules/testing-standards.md requires modules to ship with tests, and these untested paths are the gate-critical logic for changed-skill detection, review-all fallback, unreachable-base failure, and action output emission.
Generated by PR Policy Review (OpenAI) for #190 · 44.6 AIC · ⌖ 9.84 AIC · ⊞ 15.5K
| # review every skill (workflow_dispatch / initial push expect a full | ||
| # re-review). An unreachable non-empty base hard-fails rather than | ||
| # silently collapsing to "no changes". | ||
| identify_skills() { |
There was a problem hiding this comment.
rules/testing-standards.md says: "Every module gets tests — no untested code ships." This new script is now the module that enforces the Mandatory Review gate, but the added tests only drive run_reviews; they explicitly leave skill discovery (git diff / find) and the main output path unexercised. That leaves the load-bearing paths for "review all" fallback, changed-skill detection, unreachable-base hard-fail, and unreviewed-skills output writes without coverage. Please add focused tests for identify_skills/main around those cases so the extracted action body is covered end to end.
There was a problem hiding this comment.
Fixed in c3ce90d — added coverage for the gate-critical paths: identify_skills review-all fallback, git-diff changed-skill detection (against a throwaway git tree), unreachable-base hard-fail (exit 2), and main's GITHUB_OUTPUT emission (both a credit-skip and a clean run). Made review-all discovery portable (find|sed, not GNU -printf) so those paths run off the CI runner too. 30 assertions, diagnostics clean.
Addresses the #190 coverage finding: the extracted script's gate-critical paths were untested. review-skills.sh review-all discovery now uses a portable `find | sed` (BSD find has no -printf) so it runs off the CI runner, and the test adds: - identify_skills review-all fallback lists every skill dir, sorted; - identify_skills git-diff detects a modified + an added skill against a throwaway git tree; - identify_skills unreachable-base hard-fails with exit 2; - main writes the credit-skipped skill (and an empty value on a clean run) to GITHUB_OUTPUT. Config vars are exported (mirroring the action's env: contract), and the helpers keep cwd/globals isolated. 30 assertions, shellcheck + pyright clean under the .github/actions diagnostics coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Policy loaded: 23 rule files from rules/ (PR head).
Requesting changes for two test-harness comment issues under rules/boy-scout.md and rules/language-diagnostics.md: the added file’s overview is stale about discovery coverage, and its ShellCheck suppression carries a false rationale. The implementation path otherwise follows the new credit-outage carve-out shape I checked: opt-in skip, credit phrase plus 403, non-credit failures still hard-fail, warnings/summary/output are emitted for skipped skills, and diagnostics now cover .github/actions/.
Generated by PR Policy Review (OpenAI) for #190 · 46.9 AIC · ⌖ 7.62 AIC · ⊞ 15.5K
| # sourced) and drive run_reviews directly with a mocked `tessl` shell | ||
| # function — a command substitution inherits the caller's functions, so the | ||
| # mock intercepts the real call without touching PATH. Skill discovery | ||
| # (git diff / find) is not exercised here; it is a straight move of the |
There was a problem hiding this comment.
rules/boy-scout.md says the rule applies to "stale comments". This header now says skill discovery "is not exercised here", but the same added test file exercises identify_skills starting below. Please update this overview to describe the current coverage, or remove the stale discovery paragraph.
There was a problem hiding this comment.
Fixed in 34fcd10 — rewrote the header overview to describe the actual coverage (run_reviews classification, identify_skills discovery/base-failure, main output emission); the stale "discovery not exercised" paragraph is gone.
| # Run: bash .github/actions/skill-review/tests/test_review_skills.sh | ||
| # Exit 0 on all-pass; non-zero with a per-test diagnostic on failure. | ||
|
|
||
| # shellcheck disable=SC2329 # test cases run indirectly via run() ("$@" dispatch); shellcheck cannot trace dynamic invocation |
There was a problem hiding this comment.
rules/language-diagnostics.md requires that "A diagnostic is fixed, or suppressed inline with a stated reason" and that "The cause rides next to the suppression". This suppression's reason names a run() dispatcher, but this harness has no run() function and the test cases are invoked directly. Please remove the unnecessary suppression or replace it with a true, specific reason for the diagnostic being suppressed.
There was a problem hiding this comment.
Fixed in 34fcd10 — that SC2329 line was copied from another harness that does use a run() dispatcher; this one doesn't. Removed the file-top suppression and put a targeted SC2329 disable on the tessl mock instead, with the real reason: it's invoked only from run_reviews inside the sourced script, which shellcheck can't trace.
Two comment-quality findings on #190: - The header still said skill discovery "is not exercised here" after the identify_skills tests were added (rules/boy-scout.md stale comments). Rewrote the overview to describe the actual run_reviews/identify_skills/ main coverage. - The file-top SC2329 suppression cited a run() dispatcher this harness does not have (copied from another test). Removed it and added a targeted, accurate SC2329 disable on the tessl mock — the one function invoked only from the sourced script (rules/language-diagnostics.md: the cause rides next to the suppression). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Policy loaded: 23 rule files from rules/ (PR head).
Requesting changes for rule-prose self-consistency: the new Credit-Outage Review Carve-Out in rules/context-artifacts.md adds rationale/contrast prose that rules/context-writing-style.md requires rule bodies to cut or move to CHANGELOG.md. The executable action and tests otherwise line up with the declared fail-safe skip contract from the portions reviewed.
Generated by PR Policy Review (OpenAI) for #190 · 43.8 AIC · ⊞ 14.6K
| ## Credit-Outage Review Carve-Out | ||
|
|
||
| - Narrow exception for skipping review during a tessl out-of-credits (403) billing outage | ||
| - Applies when `tessl skill review` fails with the out-of-credits signature — a billing state the skill author cannot fix — never a below-threshold score |
There was a problem hiding this comment.
rules/context-writing-style.md says: "Meta-justification prose explaining why the rule exists — belongs in CHANGELOG" and "Em-dash, colon, and semicolon are not loopholes — using them to attach the same justifying clause is the same violation." This inserted clause is rationale for why the outage is acceptable, not a precondition. Please keep the rule contract here, e.g. Applies when tessl skill review fails with the out-of-credits signature; never a below-threshold score, and leave the motivation in CHANGELOG.md.
There was a problem hiding this comment.
Fixed in 66d776b — reduced to "Applies when tessl skill review fails with the out-of-credits signature; never a below-threshold score". The "billing state the author cannot fix" motivation now lives only in the CHANGELOG.
| 2. Fail-safe — only the out-of-credits signature skips; every other non-zero exit still hard-fails the publish (below-threshold score, auth error, tooling bug) | ||
| 3. Each unreviewed publish is flagged — a `::warning::` plus a `$GITHUB_STEP_SUMMARY` note names every skipped skill, surfaced on the `unreviewed-skills` action output | ||
| 4. The gate self-heals — every publish re-attempts review, so a credit top-up restores it immediately and the monthly reset restores it regardless | ||
| - Distinct from the forbidden review-step bypass in Disagreeing With the Reviewer — that dodges a verdict; this tolerates an outage that produced no verdict, opt-in and flagged |
There was a problem hiding this comment.
rules/context-writing-style.md says to cut "reinforcement and hedging restatements of conclusions the preconditions already entail" and move explanatory rationale to the changelog. The semicolon contrast (that dodges a verdict; this tolerates...) is explanatory rather than a new enforceable condition. Please rewrite this as a terse reset/constraint, for example: The forbidden review-step bypass in Disagreeing With the Reviewer remains forbidden.
There was a problem hiding this comment.
Fixed in 66d776b — rewrote as the terse constraint "The forbidden review-step bypass in Disagreeing With the Reviewer remains forbidden." The dodges-a-verdict-vs-tolerates-an-outage contrast is already in the CHANGELOG Rules entry.
Two context-writing-style findings on #190: - The Applies-when line carried an em-dash justification ("a billing state the skill author cannot fix") — rationale, not a precondition. Reduced to the scope contract: fails with the out-of-credits signature; never a below-threshold score. - The trailing "Distinct from the forbidden bypass — that dodges a verdict; this tolerates..." was explanatory contrast. Reduced to the terse constraint "The forbidden review-step bypass ... remains forbidden"; the motivation already lives in the CHANGELOG entry. Also dropped the "so ... immediately ... regardless" reinforcement from the self-heals precondition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Policy loaded: 23 rule files from rules/ (PR head).
Requesting changes for one policy violation in the new test harness: a changed comment contradicts the shell scoping it documents, which falls under rules/boy-scout.md for stale/lying comments. The credit-outage action logic otherwise follows the new carve-out shape I reviewed.
Generated by PR Policy Review (OpenAI) for #190 · 45.3 AIC · ⌖ 3.04 AIC · ⊞ 15.5K
|
|
||
| # --- identify_skills: changed-skill detection, review-all fallback, base failure --- | ||
|
|
||
| # Run identify_skills with the given event context, echoing its output. The |
There was a problem hiding this comment.
rules/boy-scout.md says this applies to "stale comments" and "lying type signatures". This comment says the config vars are local, dynamically scoped, and kept out of the global namespace, but the helper below uses export instead; capture_main later does the same while running in the main shell, so those globals do leak. Please either make these variables actually local/dynamically scoped (for example with function-local assignments if shellcheck stays clean) or rewrite the comments to describe the export/subshell behavior without claiming local scope/no global leakage.
There was a problem hiding this comment.
Fixed in 535320b — the comments were left over from the pre-export version. Rewrote both honestly: they use export (the env contract the action passes, and what keeps shellcheck from reading them as dead); capture_identify's exports stay in its $() subshell, and capture_main's do persist in the current shell but are harmless since those are the final two cases. Kept export over local because local reintroduces the SC2034 the export fixes.
The capture_identify/capture_main comments still claimed the config vars were `local` and kept out of the global namespace, but the helpers use `export` (needed for shellcheck to see them used by the sourced functions). capture_main runs in the current shell, so its exports do leak. Rewrote both comments to describe the actual export/subshell behavior honestly (rules/boy-scout.md — no lying comments): the capture_identify exports stay confined to its `$()` subshell; capture_main's persist but are harmless as the final cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous commit's comment line began "# shellcheck-clean ...", which shellcheck read as a malformed directive (SC1073/SC1072). Reworded so no comment line starts with "# shellcheck" outside the two real disable directives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Policy loaded: 23 rule files from rules/ (PR head).
All rules pass — no violations found.
Reviewed the new credit-outage action input, extracted review-skills.sh, action tests, diagnostics discovery update, publish workflow opt-in, changelog entry, and the new context-artifacts carve-out against the PR-head policy. I found no self-consistency, CI-safety, shell error-handling, testing, secret-handling, or context-artifact violations on the changed lines.
Generated by PR Policy Review (OpenAI) for #190 · 20.4 AIC · ⌖ 8.56 AIC · ⊞ 33.3K
Superseded by a later all-clear review from the same bot — dismissed by the release skill so the stale request stops gating the merge.
Author-Model: claude-opus-4-8
Summary
Implements the Ask in #188: the
skill-reviewaction gains an opt-in, fail-safecredit-outage: fail | skipinput so consumers stop vendoring the whole composite just to survive a tessl billing 403.jbaruch/nanoclaw-travelhad copiedaction.ymlverbatim (pinnedb63f13e) only to wrap the review call — exactly the drift owning the logic here is meant to prevent.This is also the PR that unblocks this repo: #184 merged but its publish failed at the skill-review step with
403 — organization has run out of credits. Withcredit-outage: skipwired intopublish.yml, that outage skips the affected skill (flagged) instead of blocking every skill-changing merge until credits reset.What changed
credit-outage: fail | skipinput (defaultfail— preserves today's Mandatory Review gate exactly).run:block into a real, unit-testedreview-skills.sh, invoked viabash "$GITHUB_ACTION_PATH/review-skills.sh"(no exec-bit dependency). Perrules/script-delegation.md— the credit-vs-real-failure classification is deterministic logic that belongs in a tested script, not an untested YAML heredoc.403, both as fixed strings (grep -F). A real failure that merely quotes "run out of credits" in prose, a different 403, or any future tessl wording change all fall through to hard-fail — the safe direction. Everything non-credit (below-threshold score, auth error, tooling bug) still hard-fails in both modes.unreviewed-skillsoutput + a$GITHUB_STEP_SUMMARYnote naming every skipped skill, so an unreviewed publish is never silent and consumers can gate follow-up steps.context-artifactsCredit-Outage Review Carve-Out documenting the sanctioned skip: opt-in, fail-safe, flagged, self-healing — mirrors theci-safetyPublish-Pipeline carve-out and is explicitly distinct from the forbidden review-step bypass.run-diagnostics.shnow shellchecks.github/actions/so the extracted script is held to the repo's zero-findings bar.publish.ymlopts intocredit-outage: skip(dogfooding).Test plan
.github/actions/skill-review/tests/test_review_skills.sh— 25 assertions, mockedtessl:fail+credits hard-fails;skip+credits tolerates and records;skip+below-threshold hard-fails;skip+phrase-without-403 andskip+403-without-phrase both hard-fail (the precision fix); mixed run records only the outage skill; deleted skill never callstessl; invalidcredit-outageexits 2.shellcheckclean onreview-skills.sh+ the test;run-diagnostics.sh(shellcheck + pyright) clean including the new.github/actions/root.{"suites":18,"passed":18,"failed":0}(was 17 — the new suite is discovered byrun-tests.sh).action.yml+publish.ymlparse as valid YAML;$GITHUB_ACTION_PATHis the correct composite-action sibling-path variable.Out of scope (deliberately)
The broader direction in #188's first follow-up — making the Mandatory Review gate satisfiable by a credit-free reviewer (promote the gh-aw policy pass, or a Claude-API rubric) to remove the tessl-credit dependency entirely — is a larger design change. This PR delivers the outage tolerance + the detection-precision fix; the credit-free-gate discussion stays open for a separate follow-up. Not using a
Closes #188keyword for that reason.🤖 Generated with Claude Code