Treat empty and whitespace-only snapshot values as missing so malformed frontmatter cannot reintroduce plausible zeroes.
Prepared with AI assistance under maintainer pbakaus's standing automation authorization.
Read the documented critique snapshot keys while preserving legacy aliases, and surface missing metrics as null instead of zero.
Prepared with AI assistance under maintainer pbakaus's standing automation authorization.
Make critique storage the single owner of snapshot discovery and frontmatter parsing, and keep context signals focused on summarizing the canonical result.
AI-assisted: Prepared by Codex under pbakaus's scheduled architecture-refactor authorization.
Round eight closes the stale-local class completely: the develop
candidate sits before the remote-default entries, so when origin/HEAD
itself points at develop, its name claim let a stale local develop win
over the fresher origin/develop. The candidate now leads with any
remote-advertised develop rev, exactly as the remote-default and
upstream candidates already lead with theirs. main/master were already
covered since their remote-default entries come first in the order.
Failing-first test forces local develop two commits behind.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Round seven: a remote-advertised default candidate tried the local
branch first, so a stale local main outranked the fresher origin/main
the symref points at and refilled changedFiles with the divergence.
The candidate now leads with the advertised remote rev, mirroring the
upstream candidate's reasoning. Failing-first test: local main forced
two commits behind the remote default, feature delta stays clean.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Round six, and the upstream-parsing ambiguity dies at the root: @{u} is
now resolved via rev-parse --symbolic-full-name, where refs/heads/...
IS a local upstream and refs/remotes/<r>/... IS remote-tracking. The
previous remote-membership heuristic still misread a local feature/foo
upstream when a remote literally named "feature" existed. The
adversarial test now configures exactly that remote and passes.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Round-five bot findings, one real root cause: splitRemoteRef treated the
first slash in any ref as a remote separator. A local upstream named
release/2.0 was truncated to "2.0", and feature/foo tracking from branch
foo collapsed to the current branch's own name and was self-skipped,
discarding a valid base both times.
The split now happens only when the prefix names a configured remote;
otherwise the whole ref is one local branch name. The per-remote HEAD
symref loop strips its own queried prefix directly (that remote may be
fabricated in tests or partial clones without appearing in git remote).
The reported pruned-upstream shape already resolves via the multi-remote
rev lists from the previous round; its test now guards that.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Cursor and Greptile converged on one root cause from the previous round:
candidate revs stopped at origin (develop tried only develop and
origin/develop; a remote-default entry carried only its own rev), so the
name-level dedup discarded a same-name base living on another remote. A
fork-parent layout with develop only as upstream/develop, or a pruned
origin/main beside a live upstream/main, lost its base entirely.
revsFor(name) now expands to the local branch plus <remote>/<name> for
every remote (origin first), and all named candidates use it, which is
exactly what makes the dedup safe. Two failing-first tests cover the
upstream-only develop and the pruned-origin/live-upstream main shapes.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two more real gaps from the post-rebase review round: a detached
checkout reads its branch as the literal HEAD, so the integration guard
never fired and candidate selection could diff a detached tip on main
against develop; and the remote-default check only consulted origin, so
a fork-parent layout whose only remote is upstream lost the guard on
its default branch entirely.
The guard now treats a detached HEAD as no-diff-base, and default-branch
symrefs are collected from every remote (origin first), feeding both the
guard and the candidate list. Two failing-first tests cover a detached
tip beside a diverged develop and an upstream-only trunk default.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Cursor Bugbot: sitting on a non-standard default such as trunk (the
origin/HEAD target) still ran candidate selection, where develop or main
could win and produce an integration-vs-integration diff. The guard now
treats the remote default branch as an integration branch alongside the
conventional names. Failing-first test: on trunk with a develop branch
present, the scope stays the working tree.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Cursor Bugbot's remaining round-1 finding held for the current code
too: in a git-flow repo whose platform default was never flipped off
main, a feature branch without an upstream picked origin/HEAD's main
over the develop branch features actually merge to, dragging the
develop-vs-main divergence into scan targets. develop now sits between
the upstream signal and origin/HEAD in the candidate order; repos
without a develop branch are unaffected. Failing-first test covers the
exact shape (develop exists, origin/HEAD -> main).
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Cursor Bugbot round two, both real: an upstream or origin/HEAD naming a
DIFFERENT integration branch bypassed the conventional-name guard, so
sitting on develop with the remote default at main still produced the
integration-vs-integration divergence this detection exists to prevent.
And splitRemoteRef returned null for a slashless @{u}, silently dropping
local upstreams (branch.<x>.remote = ".").
Base detection is now skipped entirely on an integration branch: no
signal may override the working-tree scope there. A slashless upstream
resolves as its own name and rev. Two failing-first tests: origin/HEAD
pointing at main while sitting on develop, and a feature branch
tracking a local canary branch.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Both review bots found real gaps in the first pass: candidates were
verified as local branch names only, so an origin/HEAD target with no
local checkout fell through, and stripOrigin() dropped upstreams on any
remote not named origin (fork workflows tracking upstream/release).
Candidates now carry a display name plus the revs to try in order: the
upstream's remote rev wins outright (it tracks the actual merge target,
so it beats a possibly stale local branch of the same name), origin/HEAD
tries the local branch then the remote-tracking ref, and the
conventional names each try local then origin/<name>. git.base keeps
reporting the friendly branch name while the diff runs against whichever
rev resolved. Two new failing-first tests: remote-only default branch,
and an upstream on a remote named upstream with no local base branch.
Prepared with AI assistance (Claude Code), directed by @pbakaus.
Co-Authored-By: Claude Code <noreply@anthropic.com>
context-signals hardcoded ['main', 'master'] as diff-base candidates, so
repos integrating through develop (or any other branch) diffed against
the wrong base: git.changedFiles carried the entire divergence and
downstream commands scanned the wrong set (issue #302).
The base is now detected, most specific signal first: the branch's
configured upstream (@{u}; a branch pushed with -u tracks itself and is
skipped by the self-check), then the remote's default-branch symref
(origin/HEAD), then the conventional integration names including
develop. The conventional fallbacks are withheld when the current branch
is itself one of them, so sitting on main in a repo that also has
develop keeps the working-tree scope instead of diffing two integration
branches against each other.
Five tests (three failing-first): develop-based feature branch,
origin/HEAD detection with a non-standard default name, upstream
tracking, on-the-integration-branch fallback, and the
integration-vs-integration guard.
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>
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>
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>
* Add OpenAI plugin submission bundle
Build a Codex-native OpenAI plugin with bundled hooks, public listing metadata, submission guidance, privacy coverage, and regression tests.
AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction.
* Fix provider script command rendering
Replace heuristic rewrites across executable scripts with one explicit provider marker, render pinned shortcuts per target harness, and remove the personal email from the public publisher manifest.
Addresses automated review feedback on PR #363.
AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction.
* Add a platform axis (web / ios / android / adaptive) to the skill
Orthogonal to register: register decides whether design IS or SERVES the
product; platform decides the delivery target and which native conventions
apply. Set `## Platform` in PRODUCT.md; a missing field defaults to `web`,
so legacy projects are unaffected.
- extractPlatform() in skill/scripts/context.mjs (mirrors extractRegister);
the CLI appends a NEXT STEP directive to read the native reference(s).
`adaptive` (Flutter / RN / KMP shipping both iOS and Android) loads both
ios.md and android.md.
- New reference/ios.md (Apple HIG distilled) and reference/android.md
(Material 3 distilled); reference/web.md is a thin pointer. The native
refs frame register's role as narrow: platform conformance is the bar,
brand lives in the expressive layer the platform gives you, never by
breaking the rails.
- Setup step 5 loads the native reference(s) when platform is native. Live
mode and the detect CLI stay web-only, gated off ios/android/adaptive.
- init asks platform right after register; adapt/audit/animate/layout carry
short platform divergence notes; all secondary spots thread `adaptive`.
- a11y stays in audit.md (loading it at design time makes output timid), so
the native refs carry no Accessibility section; audit.md's Platform
section owns native a11y.
- Tests: extractPlatform unit coverage + skill-behavior scenario 10
(PRODUCT.md platform ios -> agent loads ios.md).
Source-first: only skill/, scripts/, tests/, CLAUDE.md, NOTICE.md, the
changelog and version are committed; the sync workflow regenerates the
provider trees and ./plugin on merge.
ios.md / android.md are distilled from the MIT-licensed
ehmo/platform-design-skills; attribution in NOTICE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address review: gate web tools on native platforms, drop version churn
Maintainer-review fixes applied with AI assistance (Claude Code), on top
of the rebased platform-axis commit:
- Design hook (post-edit and Cursor pre-edit) now resolves the project
platform via loadContext + extractPlatform and skips its web rule scan
for ios / android / adaptive projects, so React Native / Flutter code
never draws web-shaped findings (new hook-lib resolveProjectPlatform /
isNativePlatform helpers, covered by unit and subprocess tests).
- context.mjs CLI warns on an unrecognized ## Platform value (e.g. a
toolchain name like `flutter`) instead of silently defaulting to web;
extractRegister / extractPlatform now share extractSectionValue.
- Removed reference/web.md: nothing loaded it; CLAUDE.md carries the
"web has no extra rulebook" explanation.
- init.md: skip live-mode config (Step 6) for native platforms; note the
per-app PRODUCT.md pattern for repos shipping web + native.
- android.md: Material-everywhere apps that also ship on iPhone still
owe iOS OS guarantees (safe areas, Reduce Motion, edge-swipe back).
- ios.md: reworded a design-time line that framed Dynamic Type as an
accessibility check (a11y stays owned by audit.md).
- Renumbered the new skill-behavior scenario to 14 after main's 10-13;
updated CLAUDE.md scenario list; added android + unrecognized-value
CLI test cases.
- No version or changelog changes: versioning happens at release time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Tighten platform reference prose
Editorial pass on the platform-axis text, applied with AI assistance
(Claude Code) under maintainer direction:
- ios.md / android.md rewritten to house style: single-line paragraphs
(no hard wraps), one-sentence scope intro, deduplicated intro/slop-test,
register-compression down to two sentences. In-file attribution
paragraphs removed (NOTICE.md owns attribution); "read on top of the
register reference" cruft removed (SKILL step 5 and the context.mjs
directive already say it). Bans sections dropped: they restated the
rules above them; the two additive items (tab-bar overload,
hover-dependent affordances) folded into rules. ~40% smaller each.
- Sub-command Platform sections (adapt, audit, animate, layout), SKILL
step 5, init.md platform prose, and the context.mjs directive trimmed
the same way.
Build (prose validators, counts) and both test runners green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Treat an empty PRODUCT.md section as absent, not the next heading
Copilot review catch: extractSectionValue read the next `## ...` heading
as the section value when a field was left empty, which made the CLI
warn "value `## Product Purpose` is not recognized". Stop at the next
heading and return null instead. Regression tests for extractPlatform,
extractRegister, and the CLI warning path. Applied with AI assistance
(Claude Code) under maintainer direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Only read a token list of both native targets as adaptive
Bugbot catch: after the exact platform tokens failed, any Platform line
containing the words ios and android was classified adaptive, so
negated or explanatory prose ("web only, not ios or android") silently
loaded both native refs and skipped the hook, with no warning. The
combo parse now accepts only list separators and the two platform
words; anything else falls through to the CLI's unrecognized-value
WARNING. Regression tests added. Applied with AI assistance (Claude
Code) under maintainer direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com>
Rework context-signals' detect target after review: a URL meant a costly
Puppeteer render (and a probed port might not even be this project), and the
index.html-or-bail fallback failed most real apps (no root index.html).
New priority: (1) the scannable markup/style files in the dirty git tree
(what the user is working on, small and local); (2) a local source dir
(src / app / components / pages / public — the detector walks these and skips
node_modules / dist / build); (3) a root index.html, else the project root as
a last resort when there's code. Emits `scan.targets` (a list) + `scan.via`.
Never a URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reshape of the "/impeccable suggest" proposal in #159. Instead of adding a
24th command (menu pollution + the command-add tax + its own discoverability
problem), upgrade the path users already hit: bare `/impeccable` with no
argument.
- New skill/scripts/context-signals.mjs gathers cheap, deterministic signals
(setup gaps, register, latest cached critique score, git change scope, a
dev-server port probe, and a `scan.detectTarget` for the detector) and emits
JSON. It does NOT score or rank, and it does NOT run the detector itself
(the engine isn't importable in an installed skill, and shelling npx+jsdom
would risk a hang) — the agent reasons over the raw signals.
- SKILL.md routing rule 1 now leads with the 2-3 highest-value next commands,
each with a reason from the signals, then the full menu. Never auto-runs;
always confirms. Reuses init's "Recommend starting points" vocabulary. When
a project has never been critiqued it offers critique; when scan.detectTarget
is set it runs `npx impeccable detect --fast --json` and folds the hits in.
- Export extractRegister from context.mjs for reuse.
Stays 23 commands; no metadata/pin/site-data changes. Unit-tested, including a
regression guard for porcelain leading-space path parsing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reshape of the "/impeccable suggest" proposal in #159. Instead of adding a
24th command (menu pollution + the command-add tax + its own discoverability
problem), upgrade the path users already hit: bare `/impeccable` with no
argument.
- New skill/scripts/context-signals.mjs gathers cheap, deterministic signals
(setup gaps, register, latest cached critique score, git change scope, a
dev-server port probe) and emits JSON. It does NOT score or rank — no
brittle weights table — the agent reasons over the raw signals.
- SKILL.md routing rule 1 now leads with the 2-3 highest-value next commands,
each with a reason from the signals, then the full menu. Never auto-runs;
always confirms. Reuses init's "Recommend starting points" vocabulary.
- Export extractRegister from context.mjs for reuse.
Stays 23 commands; no metadata/pin/site-data changes. Unit-tested, including
a regression guard for porcelain leading-space path parsing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>