main has been red since 2026-08-06 05:21. Three consecutive runs, one cause, unrelated to
what any of them changed:
Last green was 31072707067,
Merge #19 — the run immediately before 7bf5988.
The failure
FAILED tests/test_skills_hook.py::test_the_hook_the_symlink_points_at_actually_exists
AssertionError: skills-vendor/ is not checked out: run .skills/doctor.sh
assert False
+ where False = PosixPath('.../.claude/hooks/skills-submodule-update.sh').exists()
1 failed, 443 passed, 18 deselected
Cause
7bf5988 (#16) added the second assertion in tests/test_skills_hook.py —
test_the_hook_the_symlink_points_at_actually_exists, which requires a populated
skills-vendor/, not merely a correctly-shaped symlink. That is the right test: a dangling
link is a silent no-op, which is exactly what #16 existed to catch.
CI never populates it. .github/workflows/ci.yml line 85 is a bare
- uses: actions/checkout@v5 with no submodules: key, and actions/checkout does not
fetch submodules by default. So the symlink .claude/hooks/skills-submodule-update.sh -> ../../skills-vendor/gregoryfoster-skills/... resolves to nothing on the runner and
HOOK.exists() is False.
The first assertion in the same file passes, because is_symlink() and os.readlink() only
read the link itself. Only the one that dereferences it fails — which is why 443 of 444 pass
and the failure looks narrow.
Not the daily-refresh hold
Worth stating explicitly, because it is the intuitive read and it is wrong: this is
independent of 0f4fded (#22, suspend the daily skills refresh for the v1.2 hold). That
suspension governs a SessionStart hook that runs on a developer's machine. The CI job
never invokes it — the runner's skills-vendor/ is empty because checkout was never asked
to fetch it, not because a refresh was skipped. Lifting the v1.2 hold will leave main
red.
Fix
One key on the test job's checkout:
test:
name: test
...
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
Both submodules are public HTTPS in .gitmodules —
gregoryfoster/skills and obra/superpowers — so this needs no token, no WIF, and no
change to the existing wheelhouse auth.
The lint job (line 29) does not need it. Ruff never reads skills-vendor/, and
fetching two submodules to run a linter is cost for nothing. Scope the change to the one job
that dereferences the link.
Verification
- Push the branch and confirm
test goes green, i.e. 444 passed rather than 443.
- Confirm the
lint job is untouched and still green.
- Confirm the added fetch does not materially lengthen the run — the last green
test job was
~1m10s end to end.
Why it matters beyond the red badge
test_the_hook_the_symlink_points_at_actually_exists is currently a test that cannot pass
in CI, only locally. A check that always fails is indistinguishable from a check that
started failing, so it trains everyone to merge past a red run — which is what happened three
times, including once knowingly (#26, merged per maintainer direction with the failure noted
in the merge body). Restoring the signal matters more than the individual test does.
Found while shipping #24.
mainhas been red since 2026-08-06 05:21. Three consecutive runs, one cause, unrelated towhat any of them changed:
7bf5988#16 fix: symlink the skills SessionStart hook into the submoduleLast green was 31072707067,
Merge #19 — the run immediately before
7bf5988.The failure
Cause
7bf5988(#16) added the second assertion intests/test_skills_hook.py—test_the_hook_the_symlink_points_at_actually_exists, which requires a populatedskills-vendor/, not merely a correctly-shaped symlink. That is the right test: a danglinglink is a silent no-op, which is exactly what #16 existed to catch.
CI never populates it.
.github/workflows/ci.ymlline 85 is a bare- uses: actions/checkout@v5with nosubmodules:key, andactions/checkoutdoes notfetch submodules by default. So the symlink
.claude/hooks/skills-submodule-update.sh -> ../../skills-vendor/gregoryfoster-skills/...resolves to nothing on the runner andHOOK.exists()isFalse.The first assertion in the same file passes, because
is_symlink()andos.readlink()onlyread the link itself. Only the one that dereferences it fails — which is why 443 of 444 pass
and the failure looks narrow.
Not the daily-refresh hold
Worth stating explicitly, because it is the intuitive read and it is wrong: this is
independent of
0f4fded(#22, suspend the daily skills refresh for the v1.2 hold). Thatsuspension governs a SessionStart hook that runs on a developer's machine. The CI job
never invokes it — the runner's
skills-vendor/is empty becausecheckoutwas never askedto fetch it, not because a refresh was skipped. Lifting the v1.2 hold will leave
mainred.
Fix
One key on the
testjob's checkout:Both submodules are public HTTPS in
.gitmodules—gregoryfoster/skillsandobra/superpowers— so this needs no token, no WIF, and nochange to the existing wheelhouse auth.
The
lintjob (line 29) does not need it. Ruff never readsskills-vendor/, andfetching two submodules to run a linter is cost for nothing. Scope the change to the one job
that dereferences the link.
Verification
testgoes green, i.e. 444 passed rather than 443.lintjob is untouched and still green.testjob was~1m10s end to end.
Why it matters beyond the red badge
test_the_hook_the_symlink_points_at_actually_existsis currently a test that cannot passin CI, only locally. A check that always fails is indistinguishable from a check that
started failing, so it trains everyone to merge past a red run — which is what happened three
times, including once knowingly (#26, merged per maintainer direction with the failure noted
in the merge body). Restoring the signal matters more than the individual test does.
Found while shipping #24.