The probe asked whether node could load ESM, while the notice promised a
Node 22 floor and package.json engines declares >=22.12.0. Reviewers kept
flagging the gap, and they were right to: a 14.18-to-21 runtime passed the
probe on the strength of one import while the hook and its detector bundle
are only ever exercised on the engines floor, so "can load our code" was a
weaker claim than the one being made for it.
Check the floor directly: parseInt(process.versions.node) >= 22, in
ES5-only syntax that parses on any node old enough to fail it. Probe and
notice now derive from one NODE_MAJOR_FLOOR constant, so they cannot
disagree, and the archaeology about node: scheme support and pre-15
unhandled-rejection semantics goes with the import it explained.
Add the missing contract test: every generated hook command carries the
probe, the notice appears exactly where a harness can render it (Claude
and Codex, project and plugin), and the expected floor is read from
package.json engines rather than repeated by hand.
Verified against a fake pre-22 node, no node, and a real node: one notice
then the marker holds it silent, exit 0 in every failure shape, and the
hook's own exit code still passes through on a supported runtime.
Co-Authored-By: Claude Fable 5 (via Cursor) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Greptile flagged that the probe does not enforce the Node 22 engines floor.
Two parts to that, and they land differently.
The real defect is narrower and worse than stated: the hook closure imports
`node:fs`, `node:os`, `node:path` and `node:url`, and the `node:` scheme needs
14.18, so a bare `import('fs')` probe passed on 12 and 13 and those runtimes
then died on the real import, which is the banner this branch exists to remove.
Probing `node:fs` closes that. The added `.catch(()=>process.exit(1))` is load
bearing rather than tidiness: before Node 15 an unhandled rejection is only a
warning and the process still exits 0, so a rejected probe would have read as a
pass on exactly the versions in question.
Not enforcing 22 is deliberate and stays. The probe asks whether this runtime
can load our code, not whether it is a supported one, so a 14.18-to-21 runtime
that works today keeps working rather than being silently switched off. The
notice names 22 because that is the version worth installing, and it only ever
reaches someone whose runtime already failed the probe, so no user is shown a
threshold that contradicts what ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Commit 8397d532 took a reviewer's word that Codex expects hookSpecificOutput
and dropped its notice on that basis. Codex documents `systemMessage` for
PostToolUse and Stop as text shown as a warning in the UI or event stream,
the same field Claude Code reads, so the notice belongs there and the earlier
comment asserted something unverified.
Checked the rest against their own references while here. Cursor's preToolUse
output is permission-shaped and its user_message renders only when the action
is DENIED, so warning would mean blocking the edit. Grok treats PostToolUse
and Stop as passive events and ignores stdout outright. Copilot's contract is
unconfirmed. Those three keep the probe alone, which is a verified limit now
rather than an assumption.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bugbot caught the Codex plugin builder still invoking node directly, and the
same reasoning covers GitHub Copilot and Grok Build: all three shipped the
exact failure this branch exists to stop, and sat visibly inconsistent with
their guarded siblings.
Route them through guardedNode with no notice, matching Codex and Cursor.
GitHub gains a second property from it: outside a git repository
`$(git rev-parse --show-toplevel)` expands to nothing, so the old command
handed node a path that could not exist and failed the turn. The file test
now short-circuits that to exit 0.
Every builder carries the probe; only the two Claude manifests carry the
notice, which is the only harness whose response shape is confirmed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`systemMessage` on stdout is a Claude Code contract. The shared guard was
emitting it for Codex and Cursor too, where what a harness does with stdout
it did not ask for is unconfirmed, and a Cursor preToolUse hook printing an
unexpected JSON object is the wrong thing to guess about.
Pass the notice in per harness instead of baking it into the guard. Claude
manifests opt in; Codex and Cursor take the runtime probe alone, so an
unsupported runtime stays as quiet there as it was before the probe existed.
Giving them their own shape later is one more argument at the call site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hook command invokes bare `node`. When that node predates ESM,
`hook.mjs` dies while it is still being parsed, before the script's own
always-exit-0 contract can run, so node exits 1 and the harness reports a
hook error on every Stop and every edit.
Probe the runtime in the command string before invoking the hook, and
route the Claude plugin manifest through the guard that already covered
the project-local manifests. On probe failure the command exits 0 and
emits a one-time `systemMessage` naming the two fixes available to the
user, since nothing written in ESM can report this condition.
Fixes#410.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite runs real git subprocesses in its scratch dir, and on Node 22
the recursive afterEach delete raced git's object writes: rmdir of
.git/objects threw ENOTEMPTY and failed an unrelated PR's CI run
(seen on the #418 rebase run, checkDesignDrift suite). rmSync's
maxRetries/retryDelay options exist for exactly these transient errors.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Cursor Bugbot caught a real hole: accept unlocks at the first variant,
so a late generation agent_done for the same session id could arrive
after Accept and close the awaited failure window early, reopening the
exact #384 gap. The SSE broadcast carries no sourceEventType, so only a
carbonize agent_done is provably accept-side; the release is now gated
on it. Copilot's wording point led somewhere real too: a carbonize-phase
failure raises the same error after the source WAS promoted, so the
toast now says "may not have been saved" and normalizes the server
message's terminal punctuation. Regression guard extended to pin both.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Accept is optimistic: POST /events acknowledging the intent schedules
cleanupAcceptedSession(), which nulls pendingAcceptedSession before
live-accept.mjs has run. When the accept later failed (missing markers,
preview error, receipt conflict, source_locked), the SSE 'error' guard
keyed on pendingAcceptedSession could no longer match its id, so the
tailored recovery never fired: the user got a generic error toast, the
session was gone, and nothing said the variant was never written
(issue #384, analysis by Cursor Bugbot on #381).
Following the issue's fix sketch, an awaitingAcceptResult id is set on
the optimistic success path and deliberately survives the teardown. The
'error' case matches it and tells the user plainly that the variant was
not saved and to pick + generate again (post-teardown the wrapper may
already be gone, so restoring CYCLING is not honestly possible). The
marker is released when the real accept result arrives (complete /
accept / post-accept agent_done) or when a new session supersedes it.
Regression guard covers the set-before-teardown ordering, the error
match, and cleanupAcceptedSession leaving the marker alone; the existing
source contract now also asserts handleGo clears it.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Both bots caught the same false report: the summarize step ran off the
initial drift flag, so the no-drift-after-rebuild exit still claimed a
commit landed on main. The commit step now records pushed=true/false in
its step output and the summary reads it.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot's two review points: the fifth attempt performed a full
reset + install + rebuild + 25s backoff that nothing would ever consume
before the job failed, and the retry message blamed "main advanced"
when the combined condition also fails on push errors (network, auth).
The loop now breaks before recovery on the final attempt, and both the
retry and terminal messages name the two possible causes.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The sync workflow built once from the checked-out main and aborted when
a human commit landed during the ~30s build window (about 10% of runs
per the evidence in issue #388), leaving generated provider output
stale until the next unrelated push re-triggered it.
The commit step now loops up to five times: on a lost race it resets
hard to the fresh origin/main (source included), re-installs and
rebuilds, and pushes again with linear backoff. Every attempt therefore
builds from the main it will land on, which is the invariant the old
abort guard protected; the merge-base check stays inside the loop as
the pre-push verification. When the rebuilt output shows no drift (the
racing commit was another sync, or the new source produces identical
output) the step exits cleanly instead of committing an empty sync.
Validated by yaml-lint, bash -n, and a local three-repo simulation
(bare origin + worker + racer) confirming the lost race rebuilds
against the racer's source and lands matching output on attempt two.
Retry design proposed by @mktdgtbrz in #388; implemented from the
description with the no-drift early exit added.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
compileProviderBlocks only processes standalone-line blocks, so the
inline codex spans leaked literal tags into every provider's agent
output, degraded fallbacks included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Harnesses with no subagent capability now run each role inline from the
same single source. The build emits reference/degraded/<role>.md for every
agent in skill/agents/ (role name is the agent name minus the impeccable-
prefix), stripping frontmatter and prepending the inline-substitution
preamble. These pass through the same provider-block compilation and
placeholder replacement as ordinary reference files, so <codex> blocks and
{{placeholders}} resolve per target, and they land in the committed harness
dirs on build:release like every reference file.
Repoint the three capability-first fallback sites in the prose at the
generated files: new-work.md reviewer and documenter fallbacks, and
visualize.md asset-producer fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Probe attribution on Opus 5 showed the screenshot bound working (42
to 16) while the real burner ran free: five rounds of node -e
micro-edits, eight rebuilds, and inline defect hunts absorbed the
reviewer's and documenter's jobs until the turn cap killed the run
mid-hunt. The two-round ceiling now names scans, micro-edits, and
rebuilds; after the second round the build thread stops polishing and
ships the rest through the reviewer (one batched fix pass, one
rebuild, stop) and the documenter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both review bots caught real hazards in the migration: a symlinked
~/.opencode/skills (shared skill storage) would have its target emptied
through the link, and in a home-rooted repo that path is a live
project-scope install, not a stranded pre-#406 global copy. The
migration now requires a real directory (lstat), compares the
just-written dir by realpath instead of string, and skips entirely when
the home dir is itself a repo. Two regression tests cover the symlink
and dotfiles-repo shapes.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
npx impeccable install --providers=opencode --scope=global wrote to
~/.opencode/skills, but OpenCode discovers global skills from its config
directory: $OPENCODE_CONFIG_DIR/skills, else $XDG_CONFIG_HOME/opencode/
skills, else ~/.config/opencode/skills. The install succeeded and
`opencode debug skill` never listed it (issue #406, diagnosed by
@dergachoff).
HOME_SKILLS_DIR_OVERRIDES entries become functions of the home dir (the
Pi override from #327 was the only entry and is unchanged in behavior),
with OpenCode resolving through the env chain above. Detection gains a
resolver-based GLOBAL_HARNESS_HINTS entry so a machine with only
~/.config/opencode (no legacy ~/.opencode) still routes global installs
to OpenCode. After a global install, the skills just written are removed
from the stranded ~/.opencode/skills location; sibling skills and the
rest of ~/.opencode stay untouched, and the empty skills dir is pruned.
Four new CLI tests (failing-first): default config-dir install,
OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME precedence, legacy-copy
migration with sibling preservation, and config-dir-only detection.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot's review point stands: reference files load per-command, so a
bare "see optimize.md" / "typeset.md" is not meaningful in the routed
context. The guidance reads self-contained now.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Review bots caught two real gaps in the pseudo-stripe wiring: findings
had no source line (so line-scoped impeccable-disable directives could
not match them), and the scanner read commented-out CSS as live rules.
scanCssTextForPseudoStripe now blanks comment bodies byte-for-byte
(preserving offsets) and returns each rule's selector offset; the three
regex-engine call sites convert that to a real line, including the
whole-file line for component style blocks and CSS-in-JS templates. The
HTML path ignores the new field. Tests now assert every finding's line
against the selector's actual position and cover a commented-out stripe.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
From issue #395, the items still present after the v4 consolidation:
- optimize.md led its interactivity section with FID, retired as a Core
Web Vital in March 2024 when INP replaced it. The section heading and
both metric lists now name INP.
- optimize.md recommended react-virtualized, superseded by react-window
from the same author; the line now points at react-window and TanStack
Virtual, matching overdrive.md.
- overdrive.md's WebGPU support matrix predated Firefox 141/147 shipping
it on Windows/macOS and Safari 26 shipping it across Apple platforms.
- audit.md listed "missing will-change" as a defect while animate.md and
optimize.md both instruct applying it sparingly and never preemptively;
the audit line now flags overuse instead of absence.
- harden.md allowed 14px mobile body text while typeset.md sets a 16px
ordinary floor; harden now matches the floor, reserving 14px for
secondary text, and names the iOS Safari input-zoom consequence.
The issue's other items (Framer Motion naming, Popmotion, polish
duration cap, humor guidance, HSL phrasing in quieter) were already
resolved by the v4 reference rewrite.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Cursor Bugbot correctly noted classic VuePress keeps theme layouts,
components, and styles under .vuepress/, which the walker scanned before
the hidden-dir rule. Same treatment as .vitepress and .storybook.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The side-tab silhouette drawn as an absolutely-positioned ::before/
::after bar carries no border token, so the regex engine's line matchers
never saw it in .css/.scss files, component style blocks, or CSS-in-JS
templates — while the identical construction on a full HTML page was
flagged via checkHtmlPatterns (issue #394). Wire the existing
scanCssTextForPseudoStripe scanner into all three regex-engine paths.
New fixtures (pseudo-stripe.css, pseudo-stripe.vue) pin four flag shapes
(inset shorthand, longhand pins, bottom edge, height:100%) and six pass
shapes (neutral divider, wide panel, static, hairline, hover-conditional
underline, non-full-height badge), attributed per case via data-case
selectors in the finding snippet.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Greptile's review correctly flagged a regression in the blanket
hidden-dir skip: .vitepress/theme/*.vue and .storybook/ preview files are
real UI source that the walker scanned before this branch. Both the
walker and the scan-target filter now carry a two-entry allowlist
(HIDDEN_SOURCE_DIRS) for those conventional locations; every other
hidden dir keeps being skipped.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Review bots (Greptile, Copilot) correctly noted the aggregate count
assertion could pass if one FLAG case stopped emitting while a PASS case
started. Each flag case now carries a distinct width/radius combination
and the test deep-equals the sorted snippet list, so every finding
attributes to exactly one case.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The documented abort flow in reference/live.md (live-poll.mjs --reply <id>
error "...") reset the browser bar to PICKING but left the localStorage
checkpoint written for the GENERATING phase in place. Every reload then
resurrected a dead session the server no longer knew about, and the page
stayed wedged until the user hand-cleared the impeccable-live* keys in
the console (issue #362, diagnosed by @yourcodekitten).
An agent error reply is terminal for the session it names: when the id
matches the current session, run the same markSessionHandled + cleanup
teardown as 'discarded' (cleanup includes clearSession); when it matches
a stored-but-not-current checkpoint (the error raced a reload), drop that
checkpoint too. Errors that name no session keep the existing UI-only
reset, and the accept-cleanup and steer branches are untouched.
Regression guard added to tests/live-browser-regression.test.mjs.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
When impeccable (or any agent tool) is installed into a project's
.claude/.cursor/.codex tree, a root scan descended into the vendored skill
code and reported the detector's own example strings as findings, and
context-signals returned installed-skill files as scan candidates whenever
the harness tree appeared in the branch diff (issue #303).
Rather than growing SKIP_DIRS by a denylist of harness names that drifts
as new tools appear, the walker now skips every hidden directory during
recursion — which already covered .git/.next/.nuxt/.svelte-kit/.turbo/
.vercel, and covers all present and future harness installs plus
.impeccable itself. SKIP_DIRS shrinks to the four non-hidden entries.
An explicitly passed hidden target still scans: only child entries are
name-checked, never the root the walker is given.
scanTargets() applies the same rule to git-changed files (directory
segments only, so root dotfiles keep their existing behavior), and falls
through to source-dir targeting when the only dirty files are vendored.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The static-html engine never emitted side-tab for `border-left: 4px solid
purple` (or any named color outside a hardcoded 9-name list) in .html
files: extractStaticColor's regex dropped the color token from border
shorthands, the side defaulted to neutral black, and checkBorders skipped
it. The same declaration in a .css file was flagged by the regex engine,
so the two engines disagreed while both exited cleanly (issue #359).
Build the extraction alternation from the same CSS_NAMED_COLORS table
parseAnyColor resolves against (longest-first, whole-token), so the set of
names the extractor recognizes and the set the parser can resolve cannot
drift apart again. STATIC_NAMED_COLORS shrinks to the one keyword
parseAnyColor deliberately refuses (`transparent` as zero-alpha), since
parseAnyColor already covers every real named color in the table.
New two-column fixture (named-color-borders.html) covers the issue
reproducers: purple shorthand + radius, rebeccapurple (substring-safe
matching), crimson top stripe, bare 3px teal, var() resolving to a named
color, and an inline style attribute — with neutral named colors
(dimgray, gainsboro, black), thin, and uniform borders as pass cases.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Opus 5 turned the iterate-with-screenshots-until-it-meets-the-bar
instruction into 42 screenshot trips and 150 tool calls per build,
about forty dollars of cache churn a page, before ever reaching the
reviewer. Verification now batches: one desktop-and-mobile round after
the full build, fixes applied together, one confirming round, ceiling
two. Craft-floor's checks share those renders instead of earning
separate trips; per-tweak iteration is live mode's channel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "Codex project hooks reference hook.mjs in the .codex skill payload"
test asserted dist/codex/.codex/skills/impeccable/{SKILL.md,hook.mjs}
exist. dist/ is gitignored, and CI's test:core step runs before the
Build step, so the fresh checkout has no dist/ when the assertion runs.
It only passed locally against a stale dist/. This turned every
sync-generated-output push on main red.
The dist/codex bundle's self-consistency is already covered by
build.test.js, which runs an actual build into a temp dir and verifies
the codex payload lands at .codex/skills/. Drop the two dist assertions;
the test keeps verifying the tracked outputs (the .codex/hooks.json path
and the .agents/skills payload) that exist at test:core time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>