ci-score
CI Score gauges best-practice adherence on CI speed and gate hygiene: a
straightforward pass/fail rubric of best-practice checks computed from a
repo's workflow configuration. Eleven configuration facts, each
self-verifiable in the repo's own YAML in under a minute; score = checks
passed / applicable; one refusal (no workflows to check). Registry:
references/ci-score-spec.json (frozen
v0.1.4); rubric write-up:
references/ci-score-methodology.md.
The score measures adherence, not speed — measured, faster repos can
hold lower scores. The report says this beside the card; never let a user
read the score as a speed verdict. Nor is it a security audit: the
rubric grades best practices, and exactly two of its eleven checks
(action pinning, job-scoped OIDC tokens) happen to be security-related —
it claims nothing further about workflow security.
Requirements
- A full local checkout of the target repo. The score reads three
input classes — workflow YAML, local composite actions, and repo-root
build-tool configs — so a partial view (an API fetch of workflows alone)
silently inflates scores. The collector refuses outside a git checkout.
A full clone you make yourself is a valid path (it carries the full
input surface — the ban is on partial fetches, never on cloning): ask
before cloning (network + disk), and a score of a repo the user does
not own stays for their eyes — never published or shared.
- PyYAML (
pip install pyyaml). The scripts exit loudly with that
hint when it is missing.
- No network access is needed or used; everything reads the local tree.
Flow
All paths below are relative to this skill's directory.
Interaction contract (steps 0 and 4). Both user-facing questions are a
single structured question — one question, fixed-order options, nothing
open-ended, no machinery narration — via your platform's
structured-question tool where one exists: AskUserQuestion on Claude
Code; on Codex, its built-in user-input request tool (request_user_input
/ tool/requestUserInput, experimental — call it when exposed; check
for it and attempt the call before ever falling back, exactly as
ci-speedup does — a live ci-score Codex run skipped straight to the
plain-message branch because this contract predicted the tool would be
absent instead of trying it). Only when the tool is genuinely absent or
the call fails, ask the same question as one plain message — same options, same order, the close's save option still last
and verbatim (None, just save the report (.md)), the target confirm still
one keystroke ("Reply y to score <owner/repo>, or name a different
repo/path"). Only the delivery mechanism varies; the contract is
agent-independent. This whole contract mirrors ci-speedup's interaction
contract — same tools, same fallback shape, same verbatim save option —
keep the two parallel when either changes.
- Confirm the target before scoring. Default is the repo you are
standing in:
git rev-parse --show-toplevel is the checkout root, and
the origin remote (when GitHub) names it <owner/repo>. Ask ONE
structured question per the interaction contract — ≥ 2 options: one
confirms the detected <owner/repo> (or path when there is no GitHub
remote), one is "a different repo or path" (its pick or Other supplies
the target). When the working directory is not a git checkout
(git rev-parse fails) there is no local default to confirm — deliver
the question as the plain-message ask or an Other-only prompt (the
≥ 2-option confirm/different shape needs a detected default) and have
the user name the target (<owner/repo> or a path). If the user
already named a target, re-confirm only if ambiguous (the named target
doesn't resolve to a single local checkout or a single clonable
<owner/repo>). The confirm is load-bearing even though scoring is
local, fast, and free: any applied fix — and the report, once saved —
land in the scored repo's root, so a wrong working directory acts on
the wrong project. A pick that is another local checkout is scored
directly; a pick that is an <owner/repo> not already on disk follows
the clone-then-score rule in Requirements (confirm access with gh repo view — in sandboxed agent shells (Codex) a keyring credential can be
unreachable and gh false-fails: retry with host access before reading a
failure as no-access, mirroring ci-speedup's gh gate — then ask before
cloning — network + disk). Do not run the
collector until the target is settled.
- Collect + score (one command, writes the findings document):
python3 scripts/collect_config.py --repo <checkout-root> --out <workdir>/findings.json. <workdir> = a scratch directory outside the
target checkout — the session scratchpad if one exists, else mktemp -d;
use a per-repo subdirectory (<workdir>/<repo-name>/) so a second repo
or a re-run never overwrites a prior run's files, and never default to
cwd (an untracked findings.json inside the repo makes the next run's
provenance -dirty).
Exit 0 = scored or an honest refusal in the stamp; exit 2 = collection
refusal (not a checkout / no parseable workflows); exit 3 = scoring
failed (data_sources.ci_score_error records why). Every outcome is
stamped in the document — read it, don't guess.
- Render the report to scratch:
python3 scripts/render_report.py --findings <workdir>/findings.json --out <workdir>/report.md — score
card, the adherence-not-speed disclosure, then one recommendation per
failed check ranked by impact × risk, each with a fix recipe, its
best-practices page, and an agent handoff prompt. The rendered file
stays in <workdir> until the user asks for it (the ci-speedup
convention, issue #18 there): writing it into the target repo is what
the close's None, just save the report (.md) pick does — copy
<workdir>/report.md to ./ci-score-report.md (the target repo root)
and say where it landed in one clause (a generated artifact they can
gitignore or delete — never auto-commit it or edit their .gitignore).
No other pick writes the report into the working tree. This also
keeps the run self-clean: an unsaved report leaves no untracked file
behind to flag the NEXT run's provenance -dirty. Raw findings.json
stays in <workdir> always.
- Verify before presenting:
python3 scripts/verify_report.py --findings <workdir>/findings.json --report <workdir>/report.md must
print report: OK. If it fails, re-run step 2 and re-verify once (a
stale report.md or mismatched findings/report pair is the common cause).
If it still fails, the rendered report is unsafe to present: tell the
user verification failed (a skill bug, not their repo), give them their
result by reading the score line directly from the ci_score stamp,
and withhold the recommendations; show the violation lines only if
asked.
- Close per the close/kickoff protocol — read
references/close-contract.md first (the
"Close contract — invariants" section below is the point-of-use summary,
not the whole protocol) — two parts, both in the SAME turn:
(a) the close text (banner → disclosure → recommendations → report
note), sent as an ordinary message and never packed into the question
call, then (b) the kickoff question per the interaction contract above
— a SEPARATE structured-question tool call where one exists
(
AskUserQuestion on Claude Code); where none does (most Codex runs),
the question IS the FINAL block of that message: the numbered option
list itself, same fixed options in the same order, save option last and
verbatim, and the turn ends on it. Writing close text never completes
the close — on EITHER platform, only the question does (the tool call,
or that final printed option list). Ending the turn after (a) leaves
the user with no choices at all (live miss #2, quasar 2026-07-29: the
close referenced "the last option below" and then ended — no question
ever appeared; the user had to type their pick freehand).
Debug tracing: set STARSLING_LOG_LEVEL=DEBUG (logs counts, file names,
and check states — never file contents).
Close contract — invariants
The full close/kickoff protocol lives in
references/close-contract.md. These
non-negotiables stay here, at point of use, because burying them regressed
live runs before — in this skill, text position is behavior. Each carries its
one-line incident anchor; hold them while you follow the protocol:
- Banner: copy it VERBATIM from the collector's stdout — never redraw,
re-pad, or adjust it freehand (a hand-drawn bar mis-counted 29 of 30 blocks
in a live run). A refusal or recorded scoring error prints NO banner and
keeps its plain-sentence close — never a banner with an empty or invented
bar. Read the score straight from the
ci_score stamp; never fabricate a
score to fill a gap.
- The close = the close text, THEN the question, in the SAME turn. Writing
the close text never completes the close — on either platform, only the
question does. The options exist ONLY in the question: the
structured-question tool where the platform has one (
AskUserQuestion on
Claude Code); the final printed option list where it doesn't (most Codex
runs). Never narrate the menu in prose ahead of the question (three live
misses — quasar 2026-07-29 twice, plus a Codex run — had perfect content and
asked no question at all).
- Menu shapes. First close: up to TWO fix slots (a one-edit-closes-both
bundle counts as ONE slot; each slot carries that fix's consent scope + risk
note), then a different recommendation, then last and verbatim
None, just save the report (.md). The post-apply re-offer leads with
"Commit this branch and open a PR" — that pick IS the commit + push + PR
ask (owner 2026-07-30: a developer lands work before starting more). A
ship pick ENDS the loop; NEVER merge. A reported merge starts a fresh
banner-led round — re-score the merged base and present it as a full first
close, never a bare "re-scored, N/100" aside (live miss 2026-07-30).
- Only the save pick writes the report into the working tree (copies
<workdir>/report.md → ./ci-score-report.md); no other pick does.
- One recommendation per approval; stop at the diff. The apply "yes"
authorizes the EDIT only — never commit, push, or open a PR on its strength
(a live session treated apply as a commit license). The re-scored check is
the completion oracle; never fabricate a score and never mutate YAML just
to force a pass.
READ references/close-contract.md BEFORE
composing the close, every session — it is the executable protocol (consent
scope rules, deliberate-absence judgment, ship and post-merge rounds, refusal
closes, PR-request rules); the invariants above are what you hold in mind while
following it.
Gotchas
- Never write findings.json OR the unsaved report into the target
repo — an untracked file makes the NEXT run's provenance
-dirty
(and published profiles forbid dirty). Both render to <workdir>; only
the user's explicit save pick copies the report to
./ci-score-report.md. A saved report left untracked will honestly
read -dirty on a later run; tell the user to gitignore or delete it
first when a clean-provenance run matters.
- The score is not a speed verdict — measured, the correlation runs
the other way (the lowest-scoring repo held the fastest gate in
calibration).
The disclosure line beside the card exists for this; keep it visible.
- Path filters are the one risky recommendation — a wrong filter can
skip CI that should run, and a skipped required check blocks merges.
The report's risk note says so; repeat it when applying that fix.
- A subdirectory path still scores the whole repo — the collector
anchors to the git top level (a partial view would inflate the score),
so
--repo anywhere inside the checkout is equivalent.
-dirty provenance is conservative — untracked files count, and an
unverifiable tree (git status failing) is marked dirty, never clean.
Boundaries
ci-score grades configuration hygiene. It never measures speed, never
estimates savings, and never renders money. This ban is operational,
not just descriptive: if the user asks about speed, cost, or wall-clock
at any point — including right after their score ("so why is our CI
slow?") — do not improvise an answer from the config facts (a missing
cache is not a measurement); say so in one line and route to ci-speedup. For measured wall-clock and
runner-minute findings, that is ci-speedup — a separate skill; a
ci-speedup run never surfaces a score, and this skill never claims a
measurement. Prompt-routing contract (which skill answers what):
evals/prompt-routing.json.
1---2name: ci-score3description: Grades a repository's GitHub Actions configuration against CI best practices (the CI Score: eleven pass/fail configuration facts computed from the repo's own workflow YAML) and hands back concrete fixes for every gap, ranked by impact and risk, each with an apply-now option and a paste-able agent prompt. Use when: (1) the user asks to grade or score their CI, or audit CI configuration against best practices ("grade my CI", "CI score", "CI best practices audit", "how healthy is my CI config"), (2) re-scoring after workflow changes. Requires a local checkout of the target repository. Do NOT trigger for: CI speed, cost, or wall-clock audits, "why is CI slow", or optimization-opportunity analysis (use ci-speedup); security audits or posture review (ci-score is not a security audit — exactly two rubric checks, action pinning and OIDC token scoping, happen to be security-related); writing new workflows from scratch; non-GitHub-Actions CI.4license: MIT5---67# ci-score89**CI Score gauges best-practice adherence on CI speed and gate hygiene: a10straightforward pass/fail rubric of best-practice checks computed from a11repo's workflow configuration.** Eleven configuration facts, each12self-verifiable in the repo's own YAML in under a minute; score = checks13passed / applicable; one refusal (no workflows to check). Registry:14[references/ci-score-spec.json](references/ci-score-spec.json) (frozen15v0.1.4); rubric write-up:16[references/ci-score-methodology.md](references/ci-score-methodology.md).1718The score measures **adherence, not speed** — measured, faster repos can19hold lower scores. The report says this beside the card; never let a user20read the score as a speed verdict. Nor is it a **security audit**: the21rubric grades best practices, and exactly two of its eleven checks22(action pinning, job-scoped OIDC tokens) happen to be security-related —23it claims nothing further about workflow security.2425## Requirements2627- **A full local checkout of the target repo.** The score reads three28 input classes — workflow YAML, local composite actions, and repo-root29 build-tool configs — so a partial view (an API fetch of workflows alone)30 silently inflates scores. The collector refuses outside a git checkout.31 A **full clone you make yourself is a valid path** (it carries the full32 input surface — the ban is on partial fetches, never on cloning): ask33 before cloning (network + disk), and a score of a repo the user does34 not own stays for their eyes — never published or shared.35- **PyYAML** (`pip install pyyaml`). The scripts exit loudly with that36 hint when it is missing.37- No network access is needed or used; everything reads the local tree.3839## Flow4041All paths below are relative to this skill's directory.4243**Interaction contract (steps 0 and 4).** Both user-facing questions are a44**single structured question** — one question, fixed-order options, nothing45open-ended, no machinery narration — **via your platform's46structured-question tool where one exists**: `AskUserQuestion` on Claude47Code; on Codex, its built-in user-input request tool (`request_user_input`48/ `tool/requestUserInput`, experimental — **call it when exposed**; check49for it and attempt the call before ever falling back, exactly as50ci-speedup does — a live ci-score Codex run skipped straight to the51plain-message branch because this contract predicted the tool would be52absent instead of trying it). Only when the tool is genuinely **absent or53the call fails**, ask the **same** question as **one plain message** — same options, same order, the close's save option still **last**54and verbatim (`None, just save the report (.md)`), the target confirm still55one keystroke ("Reply **y** to score <owner/repo>, or name a different56repo/path"). Only the delivery mechanism varies; the contract is57**agent-independent**. This whole contract mirrors ci-speedup's interaction58contract — same tools, same fallback shape, same verbatim save option —59keep the two parallel when either changes.60610. **Confirm the target before scoring.** Default is the repo you are62 standing in: `git rev-parse --show-toplevel` is the checkout root, and63 the origin remote (when GitHub) names it `<owner/repo>`. Ask ONE64 structured question per the interaction contract — ≥ 2 options: one65 confirms the detected `<owner/repo>` (or path when there is no GitHub66 remote), one is "a different repo or path" (its pick or Other supplies67 the target). When the working directory is not a git checkout68 (`git rev-parse` fails) there is no local default to confirm — deliver69 the question as the plain-message ask or an Other-only prompt (the70 ≥ 2-option confirm/different shape needs a detected default) and have71 the user name the target (`<owner/repo>` or a path). If the user72 already named a target, re-confirm only if ambiguous (the named target73 doesn't resolve to a single local checkout or a single clonable74 `<owner/repo>`). The confirm is load-bearing even though scoring is75 local, fast, and free: any applied fix — and the report, once saved —76 land in the scored repo's root, so a wrong working directory acts on77 the wrong project. A pick that is another **local checkout** is scored78 directly; a pick that is an `<owner/repo>` not already on disk follows79 the clone-then-score rule in Requirements (confirm access with `gh repo80 view` — in sandboxed agent shells (Codex) a keyring credential can be81 unreachable and gh false-fails: retry with host access before reading a82 failure as no-access, mirroring ci-speedup's gh gate — then ask before83 cloning — network + disk). Do not run the84 collector until the target is settled.851. **Collect + score** (one command, writes the findings document):86 `python3 scripts/collect_config.py --repo <checkout-root> --out87 <workdir>/findings.json`. `<workdir>` = a scratch directory outside the88 target checkout — the session scratchpad if one exists, else `mktemp -d`;89 use a per-repo subdirectory (`<workdir>/<repo-name>/`) so a second repo90 or a re-run never overwrites a prior run's files, and never default to91 cwd (an untracked findings.json inside the repo makes the next run's92 provenance `-dirty`).93 Exit 0 = scored or an honest refusal in the stamp; exit 2 = collection94 refusal (not a checkout / no parseable workflows); exit 3 = scoring95 failed (`data_sources.ci_score_error` records why). Every outcome is96 stamped in the document — read it, don't guess.972. **Render the report to scratch:** `python3 scripts/render_report.py98 --findings <workdir>/findings.json --out <workdir>/report.md` — score99 card, the adherence-not-speed disclosure, then one recommendation per100 failed check **ranked by impact × risk**, each with a fix recipe, its101 best-practices page, and an agent handoff prompt. **The rendered file102 stays in `<workdir>` until the user asks for it** (the ci-speedup103 convention, issue #18 there): writing it into the target repo is what104 the close's `None, just save the report (.md)` pick does — copy105 `<workdir>/report.md` to `./ci-score-report.md` (the target repo root)106 and say where it landed in one clause (a generated artifact they can107 gitignore or delete — never auto-commit it or edit their .gitignore).108 **No other pick writes the report into the working tree.** This also109 keeps the run self-clean: an unsaved report leaves no untracked file110 behind to flag the NEXT run's provenance `-dirty`. Raw `findings.json`111 stays in `<workdir>` always.1123. **Verify before presenting:** `python3 scripts/verify_report.py113 --findings <workdir>/findings.json --report <workdir>/report.md` must114 print `report: OK`. If it fails, re-run step 2 and re-verify once (a115 stale report.md or mismatched findings/report pair is the common cause).116 If it still fails, the rendered report is unsafe to present: tell the117 user verification failed (a skill bug, not their repo), give them their118 result by reading the score line directly from the `ci_score` stamp,119 and withhold the recommendations; show the violation lines only if120 asked.1214. **Close** per the close/kickoff protocol — **read122 [references/close-contract.md](references/close-contract.md) first** (the123 "Close contract — invariants" section below is the point-of-use summary,124 not the whole protocol) — two parts, both in the SAME turn:125 (a) the close text (banner → disclosure → recommendations → report126 note), sent as an ordinary message and never packed into the question127 call, then (b) the kickoff question per the interaction contract above128 — a SEPARATE structured-question tool call where one exists129 (`AskUserQuestion` on Claude Code); where none does (most Codex runs),130 the question IS the FINAL block of that message: the numbered option131 list itself, same fixed options in the same order, save option last and132 verbatim, and the turn ends on it. **Writing close text never completes133 the close — on EITHER platform, only the question does (the tool call,134 or that final printed option list).** Ending the turn after (a) leaves135 the user with no choices at all (live miss #2, quasar 2026-07-29: the136 close referenced "the last option below" and then ended — no question137 ever appeared; the user had to type their pick freehand).138139Debug tracing: set `STARSLING_LOG_LEVEL=DEBUG` (logs counts, file names,140and check states — never file contents).141142## Close contract — invariants143144The full close/kickoff protocol lives in145[references/close-contract.md](references/close-contract.md). These146non-negotiables stay here, at point of use, because burying them regressed147live runs before — in this skill, text position is behavior. Each carries its148one-line incident anchor; hold them while you follow the protocol:149150- **Banner: copy it VERBATIM** from the collector's stdout — never redraw,151 re-pad, or adjust it freehand (a hand-drawn bar mis-counted 29 of 30 blocks152 in a live run). A **refusal or recorded scoring error prints NO banner** and153 keeps its plain-sentence close — never a banner with an empty or invented154 bar. Read the score straight from the `ci_score` stamp; **never fabricate a155 score** to fill a gap.156- **The close = the close text, THEN the question, in the SAME turn.** Writing157 the close text never completes the close — on either platform, only the158 question does. The options exist ONLY in the question: the159 structured-question tool where the platform has one (`AskUserQuestion` on160 Claude Code); the final printed option list where it doesn't (most Codex161 runs). **Never narrate the menu in prose** ahead of the question (three live162 misses — quasar 2026-07-29 twice, plus a Codex run — had perfect content and163 asked no question at all).164- **Menu shapes.** First close: up to **TWO fix slots** (a one-edit-closes-both165 bundle counts as ONE slot; each slot carries that fix's consent scope + risk166 note), then **a different recommendation**, then last and verbatim167 `None, just save the report (.md)`. The post-apply re-offer **leads with168 "Commit this branch and open a PR"** — that pick IS the commit + push + PR169 ask (owner 2026-07-30: a developer lands work before starting more). A170 **ship pick ENDS the loop; NEVER merge.** A **reported merge starts a fresh171 banner-led round** — re-score the merged base and present it as a full first172 close, never a bare "re-scored, N/100" aside (live miss 2026-07-30).173- **Only the save pick writes the report** into the working tree (copies174 `<workdir>/report.md` → `./ci-score-report.md`); no other pick does.175- **One recommendation per approval; stop at the diff.** The apply "yes"176 authorizes the EDIT only — never commit, push, or open a PR on its strength177 (a live session treated apply as a commit license). The re-scored check is178 the completion oracle; **never fabricate a score** and never mutate YAML just179 to force a pass.180181**READ [references/close-contract.md](references/close-contract.md) BEFORE182composing the close, every session** — it is the executable protocol (consent183scope rules, deliberate-absence judgment, ship and post-merge rounds, refusal184closes, PR-request rules); the invariants above are what you hold in mind while185following it.186187## Gotchas188189- **Never write findings.json OR the unsaved report into the target190 repo** — an untracked file makes the NEXT run's provenance `-dirty`191 (and published profiles forbid dirty). Both render to `<workdir>`; only192 the user's explicit save pick copies the report to193 `./ci-score-report.md`. A saved report left untracked will honestly194 read `-dirty` on a later run; tell the user to gitignore or delete it195 first when a clean-provenance run matters.196- **The score is not a speed verdict** — measured, the correlation runs197 the other way (the lowest-scoring repo held the fastest gate in198 calibration).199 The disclosure line beside the card exists for this; keep it visible.200- **Path filters are the one risky recommendation** — a wrong filter can201 skip CI that should run, and a skipped required check blocks merges.202 The report's risk note says so; repeat it when applying that fix.203- **A subdirectory path still scores the whole repo** — the collector204 anchors to the git top level (a partial view would inflate the score),205 so `--repo` anywhere inside the checkout is equivalent.206- **`-dirty` provenance is conservative** — untracked files count, and an207 unverifiable tree (git status failing) is marked dirty, never clean.208209## Boundaries210211ci-score grades configuration hygiene. It never measures speed, never212estimates savings, and never renders money. **This ban is operational,213not just descriptive:** if the user asks about speed, cost, or wall-clock214at any point — including right after their score ("so why is our CI215slow?") — do not improvise an answer from the config facts (a missing216cache is not a measurement); say so in one line and route to ci-speedup. For measured wall-clock and217runner-minute findings, that is `ci-speedup` — a separate skill; a218ci-speedup run never surfaces a score, and this skill never claims a219measurement. Prompt-routing contract (which skill answers what):220[evals/prompt-routing.json](evals/prompt-routing.json).