diff --git a/.claude/memory/blockers.md b/.claude/memory/blockers.md index b2322c5..c762f06 100644 --- a/.claude/memory/blockers.md +++ b/.claude/memory/blockers.md @@ -43,6 +43,7 @@ rules: | BLK-021 | 2026-09-22 | Bash tool dead mid-session ("every command exits 1"): /tmp usrquota blown by a dead session's probe HOMEs — 2… | open | | BLK-022 | 2026-09-22 | `hooks/guard-bash.sh` withheld by the safety classifier; executable spec shipped instead — 2026-09-22 | open | | BLK-023 | 2026-09-28 | floor-guard SKIP pattern `xit(` (Jasmine) matches any `exit(` in python/JS test helpers → false ECARTS; workaround: no `exit(` in inline python, bash derives rc from output — 2026-09-28 | resolved | +| BLK-024 | 2026-09-29 | update-all.sh re-fetched vendored skills but never re-applied the effort pins (lost until next `make plugin`); my first fix placed the re-apply BEFORE the late 21st refresh — rtk-truncated grep read as complete — 2026-09-29 | resolved | --- @@ -268,3 +269,9 @@ rules: - **Real cause**: `lib/floor-guard.sh` SKIP_SUBSTRINGS holds the bare fragment `'xit('` to catch Jasmine's `xit(…)`; `skip_kind()` is a plain substring match, so `sys.exit(`, `SystemExit(`, `process.exit(` all hit. - **Solution**: workaround applied — the inline python prints violations only, the bash wrapper derives the return code from the captured output (no `exit(` anywhere). Root fix pending: word-bound the pattern (`(^|[^a-zA-Z_.])xit\(`) or match `xit(` only in JS/TS test files; hotfix-sized. - **Status**: resolved 2026-09-28 — hotfix 0deb559 (bugfix/floor-guard-xit-boundary): the four bare Jasmine identifiers moved into `SKIP_IDENT_RE` with lookbehind `(?)` always travels with the step's first tool call, shift first; pair a downward shift with a pinned executor or a Read/Bash, never with a built-in judgment dispatch; before any built-in judgment dispatch, pair the own-level shift with it; skills Claude loads alone do not apply their pin → re-assert with a paired shift at the resumed planning step ([[BDR-107]]). + +## LRN-181 — Stacked skills share one effort level; a lone load applies none +- **Context**: design toolchain loads 5-8 skills in one build. Skill `effort:` frontmatter = last loaded wins, both directions ([[LRN-179]]). Two levels inside the stack → effort depends on load order, invisible. Plus [[LRN-180]]: a Skill call Claude issues alone is a no-op. +- **Apply**: one level per stack (`lib/effort-pins.txt` design section, census `stack_levels` lock, site-motion frontmatter matches); doctrine "load the stack paired with the first Read of the target file"; new vendored design skill → copy the stack level. [[BDR-108]] + +## LRN-182 — Effort baselines are generation-bound; model aliases move silently +- **Context**: transcripts of the last weeks show `sonnet` → claude-sonnet-5 (3069 msgs) then claude-sonnet-5-5 (recent), `opus` → opus-5 then opus-5-5, `fable` → fable-5 then fable-5-1. API reference: Sonnet 5.5 recalibrated effort levels ("start at medium for agentic coding"). [[EVAL-036]] A/B ran on one generation. +- **Apply**: after an alias moves (new model in a tier) re-run `python3 lib/effort-audit.py` and re-read the pins; write the generation next to any effort figure; keep aliases (latest = cheapest or same price, never pin a version for a measurement). [[BDR-108]] diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index b6c4f18..6ef2144 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -1,5 +1,27 @@ # TODO +## 2026-09-29 — effort round: every skill carries a level next to its model pin (feature/effort-round) +User table: low fix-a-line/run-a-script · medium day-to-day · high refactor/resisting bug · +xhigh architecture/audit before validation · max stuck. Approved 2026-09-29: design stack +high uniform, hotfix stays high, all vendored externals of the table, docs in the same branch. +Model pins stay aliases (latest of each tier is also the cheapest or same price); the +quality/price trade-off is tier × effort, never version. +- [x] S1 `lib/effort-pins.txt` (map) + `lib/effort-pins.sh` (idempotent re-apply) replacing the + hardcoded brainstorming/writing-plans loop; called after the last vendoring step of + install-plugins.sh AND update-all.sh (resync dropped the pins until the next make plugin) +- [x] S2 repo skills: skills-perso low, pdf-translate medium, site-motion high +- [x] S3 tests: `lib/tests/effort-pins.test.sh` (fixture: insert, keep, replace, skip, reject) + + effort-routing census map-driven + design-stack uniformity lock +- [x] S4 `lib/effort-audit.py`: count records without output_tokens_details, print coverage + (sub-agent thinking was read as 0 on ~90 % of records: a gap, not a finding) +- [x] S5 doctrine: Design work paired load + one level per stack (CLAUDE.global.md, lib/effort-shift.md) +- [x] S6 docs: README effort section, USAGE niveau d'effort, CHANGELOG +- [x] S7 contract + GATE 0 + fresh verifier + security gate, make test, shellcheck — GATE 0 MET, verifier ECARTS(3) → executor moved the resync re-apply after the 21st refresh (real gap), scope gated, directive authorized → CONFORME 7/7; security PASS (4 LOW on the helper, see journal); make test 44 suites rc 0 +- [x] S8 registries BDR-108, LRN-181, LRN-182, BLK-024, EVAL-038 (user go) + journal +- [x] S9 hardening of lib/effort-pins.sh (4 LOW, user go): fresh executor, T11-T14, verifier CONFORME 9/9, security PASS +- [ ] parked LOW (security re-gate 2026-09-29, none exploitable): no RETURN trap on the mktemp sibling (SIGINT during awk leaves `SKILL.md.XXXXXX`); T13 never reaches the post-write re-read branch (CRLF opener fails `_effort_pin_closed` first, fixture with LF delimiters + CRLF `name:` line would); T14 fails under root (chmod ignored); `WORK="$(mktemp -d)"` unguarded in the suite (`|| exit 1`); install-plugins.sh `err()` uses `echo -e` on the rejected map line +- UNMERGED — human gate ("merge it") + ## 2026-09-28 — effort tiering: session high, agent pins, skill levels, phase shifts (feature/effort-tiering) Spec `docs/superpowers/specs/2026-09-28-effort-tiering-design.md`, plan `docs/superpowers/plans/2026-09-28-effort-tiering.md`. Approved 2026-09-28: session diff --git a/.claude/tasks/contracts/2026-09-29-effort-round-1315.md b/.claude/tasks/contracts/2026-09-29-effort-round-1315.md new file mode 100644 index 0000000..cb066d4 --- /dev/null +++ b/.claude/tasks/contracts/2026-09-29-effort-round-1315.md @@ -0,0 +1,63 @@ +# CONTRACT — effort-round +- date: 2026-09-29 | flow: feat by hand (feature/* off develop) | branch: feature/effort-round +- status: active + +## REQUEST (verbatim — IMMUTABLE) +> en se basant sur le meme tableau que la derniere fois [low: corriger une ligne, renommer un fichier, lancer un script · medium: le travail courant · high: un refactor, un bug qui resiste · xhigh: architecture, audit avant validation · max: quand une erreur coince, une erreur ne se rattrape pas, ou qu'on juge avoir besoin de beaucoup de reflexion], quand on a pin les orchestrateurs et leur sous agent a des efforts, j'aimerais que tu fasse une ronde de tout les skill et que tu mete un niveau d'effort en plus du model pin. D'ailleurs les model pin, c'est du par exemple Sonnet ou du Sonnet 5.5 (version du model pinned) ? Car il faudrait utiliser les versions qui vont bien avec la tache qu'ils ont a acomplir. +> [answered: model pins stay tier aliases; the latest version of a tier is also the cheapest or same-priced, the quality/price trade-off is tier × effort] + +## CLARIFICATIONS +- User choices 2026-09-29 (AskUserQuestion): design stack high uniform; hotfix stays high; every vendored external of the proposed table gets a pin; README/USAGE docs in the same branch. +- Round result: 30 existing entry levels hold against the table; 3 repo skills had none (skills-perso low, pdf-translate medium, site-motion high); vendored externals get theirs from `lib/effort-pins.txt` re-applied by `lib/effort-pins.sh`; impeccable, graphify, find-docs, gstack, darwin-skill and the five shifters stay unpinned (machine-owned, BDR-107). +- Defect found in passing, fixed here: `update-all.sh` re-fetched the vendored skills but never re-applied the pins (lost until the next `make plugin`). +- Defect found in passing, surfaced not fixed: ~94 % of sub-agent usage records carry no `output_tokens_details`, so `lib/effort-audit.py` read zero thinking on sub-agents; the script now prints coverage and a CAVEAT; EVAL-037's "executors stay cheap" is a measurement gap (registry correction pending user approval). +- lib/tests/effort-routing.test.sh line 4 widens its shellcheck directive from SC2015 to SC2015,SC2016: the new `has … '$REPO'` locks are literal source text, the `$REPO` must NOT expand (authorized; a test file, informational). [verifier 2026-09-29 gap 3] +- Hardening round (criteria 8-9) added after the security gate on user go; the fixture suite may `chmod` its own mktemp directory (555 then back to 755 for the trap cleanup), never `-R`, never outside the fixture. +- Frontmatter placement of the inserted `effort:` line (after `name:`, else before the closing `---`) has no harness effect; locked by the fixture suite only. + +## ACCEPTANCE CRITERIA +1. Map + helper: `lib/effort-pins.sh` inserts, keeps, replaces (frontmatter only), skips a missing skill, is idempotent, rejects a bad level / traversal name / three-field line before writing, parses the real map. + CHECK: out=$(make test suite=lib/tests/effort-pins.test.sh 2>&1); echo "$out" | grep -q 'effort-pins: [0-9]* pass, 0 fail' || { echo "$out" | grep FAIL; exit 1; }; echo PINS_GREEN + EXPECT: PINS_GREEN + EVIDENCE: MET exit=0 marker-found :: PINS_GREEN +2. Census: the effort-routing suite is green and locks the three new repo levels, the map-driven vendored check, the design-stack single level, both re-apply call sites and the doctrine pointer. + CHECK: out=$(make test suite=lib/tests/effort-routing.test.sh 2>&1); echo "$out" | grep -q 'census: [0-9]* pass, 0 fail' || { echo "$out" | grep FAIL; exit 1; }; for k in skills-perso pdf-translate site-motion effort-pins.txt 'stack_levels' 'apply_effort_pins'; do grep -q "$k" lib/tests/effort-routing.test.sh || { echo "census lacks $k"; exit 1; }; done; echo CENSUS_GREEN + EXPECT: CENSUS_GREEN + EVIDENCE: MET exit=0 marker-found :: CENSUS_GREEN +3. Re-apply wired after the LAST vendoring step of both scripts, hardcoded loop gone: in install-plugins.sh the call follows the 21st pack staging block; in update-all.sh it follows the 21st pack refresh (§7.4, the last step that rewrites a SKILL.md), which itself follows the superpowers refresh. [verifier 2026-09-29: the first placement sat after the superpowers refresh only, the 21st refresh ran later and dropped seven pins] + CHECK: a=$(grep -n 'apply_effort_pins "$REPO"' install-plugins.sh | cut -d: -f1); b=$(grep -n 'rm -rf "$TFD_STAGE"' install-plugins.sh | tail -1 | cut -d: -f1); c=$(grep -n 'apply_effort_pins "$REPO"' update-all.sh | cut -d: -f1); d=$(grep -n 'skills-external/$_tfd_name' update-all.sh | tail -1 | cut -d: -f1); e=$(grep -n 'vendor_pinned_skills superpowers refresh' update-all.sh | cut -d: -f1); [ "$(echo "$a" | wc -l)" -eq 1 ] && [ "$a" -gt "$b" ] && [ "$(echo "$c" | wc -l)" -eq 1 ] && [ -n "$d" ] && [ "$c" -gt "$d" ] && [ "$c" -gt "$e" ] && ! grep -q 'for _s in brainstorming writing-plans' install-plugins.sh && bash -n install-plugins.sh && bash -n update-all.sh && echo RESYNC_OK + EXPECT: RESYNC_OK + EVIDENCE: MET exit=0 marker-found :: RESYNC_OK +4. Live tree: every map entry whose skill is vendored on this machine carries that level in its frontmatter (idempotent re-run applies 0). + CHECK: out=$(bash lib/effort-pins.sh 2>&1) && echo "$out" | grep -q ' 0 applied, [0-9]* already at level' && echo LIVE_AT_LEVEL + EXPECT: LIVE_AT_LEVEL + EVIDENCE: MET exit=0 marker-found :: LIVE_AT_LEVEL +5. Audit script: compiles, runs on a fixture with two records lacking `output_tokens_details` and one carrying it, reports 33 % coverage for that scope and the CAVEAT line (below 50 %). + CHECK: python3 -m py_compile lib/effort-audit.py && D=$(mktemp -d) && mkdir -p "$D/p" && printf '%s\n%s\n%s\n' '{"type":"assistant","message":{"id":"m1","model":"claude-sonnet-5-5","usage":{"input_tokens":1,"output_tokens":10}}}' '{"type":"assistant","message":{"id":"m2","model":"claude-sonnet-5-5","usage":{"input_tokens":1,"output_tokens":10}}}' '{"type":"assistant","message":{"id":"m3","model":"claude-sonnet-5-5","usage":{"input_tokens":1,"output_tokens":10,"output_tokens_details":{"thinking_tokens":4}}}}' > "$D/p/s.jsonl" && out=$(python3 lib/effort-audit.py "$D") && echo "$out" | grep -q 'thinking counted on 33% of them' && echo "$out" | grep -q 'CAVEAT: main' && echo AUDIT_OK + EXPECT: AUDIT_OK + EVIDENCE: MET exit=0 marker-found :: AUDIT_OK +6. Doctrine + docs: CLAUDE.global.md ≤ 320 lines with the paired-load line; README "## Effort routing"; USAGE "### Niveau d'effort"; CHANGELOG Added + Fixed entries; doctrine-citers census green. + CHECK: [ "$(wc -l < CLAUDE.global.md)" -le 320 ] && grep -q 'a lone Skill call applies no effort' CLAUDE.global.md && grep -q '^## Effort routing' README.md && grep -q "^### Niveau d'effort" USAGE.md && grep -q 'Effort round (BDR-108)' CHANGELOG.md && grep -q 'never re-applied the effort pins' CHANGELOG.md && make test suite=lib/tests/doctrine-citers.test.sh 2>&1 | grep -q 'FAIL=0' && echo DOCS_OK + EXPECT: DOCS_OK + EVIDENCE: MET exit=0 marker-found :: DOCS_OK +7. Health stack: shellcheck clean on the touched shell files and the Health Stack set; no-vacuous-locks green. + CHECK: shellcheck lib/effort-pins.sh lib/tests/effort-pins.test.sh lib/tests/effort-routing.test.sh install-plugins.sh update-all.sh *.sh hooks/*.sh lib/*.sh && make test suite=lib/tests/no-vacuous-locks.test.sh >/dev/null 2>&1 && echo LINT_OK + EXPECT: LINT_OK + EVIDENCE: MET exit=0 marker-found :: LINT_OK + +8. Hardening (security gate 2026-09-29, 4 LOW, user go): (a) a map whose last line has no trailing newline still applies that line; (b) a SKILL.md whose frontmatter has no closing `---` is skipped with an err line, file byte-identical; (c) a CRLF SKILL.md (`---\r`) is never counted as applied: the helper re-reads the level after the write and reports a mismatch as err, counted as failed (rc 1); (d) a write failure (read-only skill directory) is reported as err, counted as failed, and leaves no temporary file behind. Cases T11-T14 in lib/tests/effort-pins.test.sh, header comment of the helper updated. + CHECK: out=$(make test suite=lib/tests/effort-pins.test.sh 2>&1); echo "$out" | grep -q 'effort-pins: [0-9]* pass, 0 fail' || { echo "$out" | grep FAIL; exit 1; }; for k in T11-last-line-no-newline T12-unterminated-frontmatter-skipped T13-crlf-not-counted-applied T14-write-failure-no-temp; do echo "$out" | grep -q "PASS $k" || { echo "missing PASS $k"; exit 1; }; done; echo HARDEN_GREEN + EXPECT: HARDEN_GREEN + EVIDENCE: MET exit=0 marker-found :: HARDEN_GREEN +9. Hardening keeps everything else green: shellcheck clean on the helper and its suite, effort-routing census green, live tree still idempotent (0 applied). + CHECK: shellcheck lib/effort-pins.sh lib/tests/effort-pins.test.sh && make test suite=lib/tests/effort-routing.test.sh 2>&1 | grep -q 'census: [0-9]* pass, 0 fail' && bash lib/effort-pins.sh 2>&1 | grep -q ' 0 applied, [0-9]* already at level' && echo HARDEN_STABLE + EXPECT: HARDEN_STABLE + EVIDENCE: MET exit=0 marker-found :: HARDEN_STABLE + +## FILE SCOPE +- lib/effort-pins.txt, lib/effort-pins.sh (new); lib/tests/effort-pins.test.sh (new); lib/tests/effort-routing.test.sh +- install-plugins.sh, update-all.sh (re-apply call), lib/effort-audit.py (coverage) +- skills/skills-perso/SKILL.md, skills/pdf-translate/SKILL.md, skills/site-motion/SKILL.md (effort line) +- lib/effort-shift.md, CLAUDE.global.md (doctrine), README.md, USAGE.md, CHANGELOG.md +- .claude/tasks/TODO.md, .claude/tasks/contracts/ (this file) +- skills-external/design-motion-principles/SKILL.md [gated 2026-09-29] — the only vendored external tracked in git; its copy carries the `effort: high` line the resync re-applies (user choice: gate, not untrack) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d64d6..024d97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added +- **Effort round (BDR-108)**: every skill carries an entry level next to its model pin. `lib/effort-pins.txt` (map) + `lib/effort-pins.sh` (idempotent re-apply after the last vendoring step of `install-plugins.sh` and `update-all.sh`) replace the hardcoded brainstorming/writing-plans loop and extend the pins to the design stack (high, one level per stack since the last loaded wins), superpowers, agent-skills and the 21st pack; `skills-perso` low, `pdf-translate` medium, `site-motion` high; doctrine: the design stack loads paired with the first Read (a lone Skill call applies nothing). Model pins stay tier aliases: the latest version of a tier is also the cheapest or same-priced, so the quality/price trade-off is tier × effort, never version. `lib/effort-audit.py` prints thinking coverage per scope (sub-agent records carry no thinking count on ~90 % of requests: EVAL-037's "executors stay cheap" was a measurement gap, not a finding). - **Effort tiering (BDR-107)**: reasoning effort routed per role and per phase. Session default `high`; `effort:` pins on the 20 repo-authored agents; entry level on 28 tracked user-invoked skills plus the two vendored superpowers skills (re-applied by `install-plugins.sh` after resync); five shifter skills `effort-low` … `effort-max` loaded at phase boundaries per `lib/effort-shift.md`, always sent with the step's first tool call (a lone Skill call is a no-op on 2.1.283), with `max` at the verify-secure caps and ship-feature 4b; `/effort-max` as the turn-scoped relaunch lever; statusline shows the live level; session banner warns when `CLAUDE_CODE_EFFORT_LEVEL` silences the pins; census `lib/tests/effort-routing.test.sh`; transcript audit `lib/effort-audit.py`. - **Design gate asks the user to sign in to 21st instead of skipping it**: `lib/design-tool-gate.sh` adds a three-state 21st auth predicate @@ -462,6 +463,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). plugin cache or `claude plugin list`. ### Fixed +- `update-all.sh` re-fetched the vendored skills at every run but never re-applied the effort pins: brainstorming/writing-plans lost their xhigh until the next `make plugin` (BDR-107 gap, closed by `lib/effort-pins.sh`). - **gitflow pre-commit blocked every commit with gitleaks 8.16** (Ubuntu's apt package): the hook ran `gitleaks git --staged`, a subcommand that exists from 8.19 only, so the "unknown command" exit 1 read as a leak. The generator now diff --git a/CLAUDE.global.md b/CLAUDE.global.md index 220c268..4882aea 100644 --- a/CLAUDE.global.md +++ b/CLAUDE.global.md @@ -283,6 +283,10 @@ design routing; the design-toolchain hook reinforces it. - Design system / brand → design-consultation first, then the build tools. - Review / audit → design-review + emil-design-eng + design-motion-principles + /impeccable audit|critique + `impeccable detect` floor. +- Load the stack paired with the first Read of the target file, never + alone (a lone Skill call applies no effort, `lib/effort-shift.md`); every + vendored member pins `high`, one level per stack (`lib/effort-pins.txt`); + plugin and gstack members run at the level in force. Scope doubt → ask or default to Build, never silently skip. Gate: light skills run `~/.claude/lib/design-gate.md`, orchestrators plugin-check. 21st = CLI (`npm i -g @21st-dev/cli`, `21st login`), no MCP, no key; search free, diff --git a/README.md b/README.md index 3a5ad36..870d9c3 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,23 @@ was split like `/feat` (reflection inline + gate, `hotfixer` executor) and so joins the gated group (13th); `/client-handover`'s nested skill-runner children are dispatched `model:"fable"` (they carry reflection). +## Effort routing (BDR-107, BDR-108) + +Second axis of the same table: how hard each phase thinks. Session default +`high`. Every typed agent carries an `effort:` pin next to its `model:` (low +appliers, medium executors, high judgment, xhigh challengers and gates; none +on haiku, which rejects the parameter). Every user-invoked skill carries an +entry level (`/status` low … `/ship-feature` xhigh); the vendored externals +(design stack, superpowers, agent-skills, 21st) get theirs from +`lib/effort-pins.txt`, re-applied by `lib/effort-pins.sh` after every +vendoring step. Orchestrators shift per phase through the `effort-low` … +`effort-max` skills (`lib/effort-shift.md`, always sent with another tool +call: a lone Skill call applies nothing). Model pins stay tier aliases +(`sonnet`, `opus`, `haiku`, `fable`): the latest version of a tier is also +the cheapest or same-priced, so the quality/price trade-off is tier × effort, +never version. Census `lib/tests/effort-routing.test.sh`; transcript audit +`python3 lib/effort-audit.py`. + --- ## Install notes diff --git a/USAGE.md b/USAGE.md index 55fe619..86087d3 100644 --- a/USAGE.md +++ b/USAGE.md @@ -171,6 +171,20 @@ Tu veux... --- +### Niveau d'effort + +Chaque commande démarre à un niveau de réflexion fixé dans son frontmatter +(`effort:`) : low pour la tenue de registre (`/status`, `/close`, +`/commit-change`), medium pour le courant (`/gitflow`, `/prune-memory`), +high pour un fix ou un refactor (`/feat`, `/hotfix`, `/bugfix`, `/refactor`, +audits avec fix), xhigh pour l'architecture et l'audit avant validation +(`/ship-feature`, `/onboard`, `/analyze`). Les orchestrateurs décalent +ensuite le niveau par phase (`lib/effort-shift.md`), et `/effort-max` tapé à +la main relance un tour bloqué au maximum. Les skills externes vendorés +(pile design, superpowers, 21st) reçoivent leur niveau de +`lib/effort-pins.txt`. Un skill chargé seul par Claude n'applique pas son +niveau : il doit partir avec un autre appel d'outil dans le même message. + ## Les plugins — décision rapide ``` diff --git a/install-plugins.sh b/install-plugins.sh index 935ec9e..1d14597 100644 --- a/install-plugins.sh +++ b/install-plugins.sh @@ -934,16 +934,9 @@ for _ext_skill in "${EXT_SKILL_NAMES[@]}"; do done echo "" -# Effort tiering (BDR-107): the vendored brainstorming/writing-plans carry an -# effort pin upstream lacks; re-apply after every resync (census lock in -# lib/tests/effort-routing.test.sh alarms if this ever stops working). -for _s in brainstorming writing-plans; do - _f="$(cd "$(dirname "$0")" && pwd)/skills-external/$_s/SKILL.md" - if [ -f "$_f" ] && ! grep -q '^effort:' "$_f"; then - sed -i "0,/^name: $_s\$/s//&\neffort: xhigh/" "$_f" - fi -done -unset _s _f +# Effort pins (BDR-107, BDR-108): every vendored external gets its entry +# level from lib/effort-pins.txt, re-applied ONCE after the last vendoring +# step (the 21st pack, STEP 8.7) — see apply_effort_pins there. # ============================================================ # STEP 8.5 — EXTERNAL SKILLS (npx skills add …) @@ -1062,6 +1055,13 @@ if command -v 21st &>/dev/null; then rm -rf "$TFD_STAGE" fi +# Effort pins (BDR-107, BDR-108): the vendored externals carry no `effort:` +# upstream and every vendoring step above rewrites SKILL.md. Re-apply the +# entry levels from lib/effort-pins.txt once, after the LAST such step. +# shellcheck source=lib/effort-pins.sh disable=SC1091 +source "$REPO/lib/effort-pins.sh" +apply_effort_pins "$REPO" || warn "effort pins: map lines rejected — fix lib/effort-pins.txt" + # Auth — detect, then offer login ONLY in an interactive TTY. A non-interactive # run (CI / headless / re-run) must never open a browser or block on OAuth. # Search and logo lookup are free; retrieving component code and 21st AI need diff --git a/lib/effort-audit.py b/lib/effort-audit.py index 2d84556..c3ee7c1 100755 --- a/lib/effort-audit.py +++ b/lib/effort-audit.py @@ -10,18 +10,21 @@ import sys # Weights relative to input price. WEIGHTS = {"in": 1.0, "cc": 1.25, "cr": 0.1, "out": 5.0} -FIELDS = ("in", "cc", "cr", "out", "think") +FIELDS = ("in", "cc", "cr", "out", "think", "nodet") def usage_row(usage): - """Map one API usage block to the five counted fields.""" - details = usage.get("output_tokens_details") or {} + """Map one API usage block to the counted fields. `nodet` marks a + record whose usage carries no output_tokens_details at all: no thinking + count was recorded (most sub-agent records), so `think` understates.""" + details = usage.get("output_tokens_details") return { "in": usage.get("input_tokens", 0) or 0, "cc": usage.get("cache_creation_input_tokens", 0) or 0, "cr": usage.get("cache_read_input_tokens", 0) or 0, "out": usage.get("output_tokens", 0) or 0, - "think": details.get("thinking_tokens", 0) or 0, + "think": (details or {}).get("thinking_tokens", 0) or 0, + "nodet": 0 if details else 1, } @@ -57,21 +60,27 @@ def weighted(counter): return sum(counter[f] * WEIGHTS[f] for f in WEIGHTS) -def report(agg): - """Print the per-key table, then the main/sub split and the thinking - share.""" - total = collections.Counter() - for counter in agg.values(): - total.update(counter) - total_w = weighted(total) or 1 +def coverage(counter): + """Share of requests whose usage carries a thinking count.""" + return 100 * (1 - counter["nodet"] / max(counter["msgs"], 1)) + + +def print_rows(agg, total_w): + """One line per (scope, model, effort), costliest first.""" print(f"{'scope':5} {'model':22} {'effort':7} {'msgs':>6} {'think/msg':>9} " - f"{'think_tok':>10} {'out_tok':>10} {'cache_read':>12} {'%wcost':>7}") + f"{'think_tok':>10} {'out_tok':>10} {'cache_read':>12} {'%wcost':>7} " + f"{'%counted':>8}") ranked = sorted(agg.items(), key=lambda kv: -weighted(kv[1])) for (scope, model, effort), c in ranked: per_msg = c["think"] / max(c["msgs"], 1) print(f"{scope:5} {model:22} {effort:7} {c['msgs']:6d} " f"{per_msg:9.0f} {c['think']:10d} {c['out']:10d} " - f"{c['cr']:12d} {100 * weighted(c) / total_w:6.1f}%") + f"{c['cr']:12d} {100 * weighted(c) / total_w:6.1f}% " + f"{coverage(c):7.0f}%") + + +def print_scopes(agg, total, total_w): + """Main/sub split, thinking share and the coverage caveat.""" by_scope = collections.defaultdict(collections.Counter) for (scope, _, _), c in agg.items(): by_scope[scope].update(c) @@ -79,11 +88,27 @@ def report(agg): print(f" {scope:5} weighted-cost " f"{100 * weighted(c) / total_w:5.1f}% thinking " f"{100 * c['think'] / max(total['think'], 1):5.1f}% " - f"requests {c['msgs']}") + f"requests {c['msgs']} thinking counted on " + f"{coverage(c):.0f}% of them") print(f" thinking = " f"{100 * total['think'] * WEIGHTS['out'] / total_w:.1f}% " f"of weighted cost; cache reads = " f"{100 * total['cr'] * WEIGHTS['cr'] / total_w:.1f}%") + low = [s for s, c in by_scope.items() if coverage(c) < 50] + if low: + print(f" CAVEAT: {', '.join(low)} records mostly carry no thinking " + f"count — their think columns are a floor, not a measure") + + +def report(agg): + """Print the per-key table, then the main/sub split and the thinking + share.""" + total = collections.Counter() + for counter in agg.values(): + total.update(counter) + total_w = weighted(total) or 1 + print_rows(agg, total_w) + print_scopes(agg, total, total_w) def main(): diff --git a/lib/effort-pins.sh b/lib/effort-pins.sh new file mode 100755 index 0000000..3edb209 --- /dev/null +++ b/lib/effort-pins.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# lib/effort-pins.sh — re-apply the entry effort level on vendored skills +# (BDR-107 second axis, extended to every vendored external by BDR-108). +# Upstream copies carry no `effort:` and every vendoring step rewrites +# SKILL.md, so the level lives in lib/effort-pins.txt and this helper puts +# it back after the last vendoring step of install-plugins.sh and +# update-all.sh. Idempotent: same level → untouched, other level → +# replaced inside the frontmatter only, skill not vendored → skipped, +# malformed map line → rejected loudly, never applied. Four hardenings: +# a map whose last line lacks a newline is still read; a SKILL.md whose +# frontmatter never closes is skipped untouched; the level is re-read after +# every write and a mismatch (CRLF, malformed) counts as failed; the write +# goes through a mktemp sibling removed on any failure. Placement inside the +# frontmatter has no effect on the harness, which reads the key anywhere. +# +# Usage: source it, then `apply_effort_pins [repo-root]` +# or standalone: bash lib/effort-pins.sh [repo-root] +# Exit 1 when at least one map line was rejected or a skill failed. + +EFFORT_PINS_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +EFFORT_PIN_LEVEL_RE='^(low|medium|high|xhigh|max)$' +EFFORT_PIN_NAME_RE='^[A-Za-z0-9][A-Za-z0-9._-]*$' + +# Callers (install-plugins.sh, update-all.sh) define these; standalone +# runs get plain fallbacks. +declare -F ok >/dev/null || ok() { printf ' ok %s\n' "$*"; } +declare -F info >/dev/null || info() { printf ' info %s\n' "$*"; } +declare -F err >/dev/null || err() { printf ' ERR %s\n' "$*" >&2; } + +# _effort_pin_current → prints the frontmatter effort, if any +_effort_pin_current() { + awk 'NR==1&&/^---$/{p=1;next} p&&/^---$/{exit} p' "$1" \ + | sed -n 's/^effort: //p' | head -1 +} + +# _effort_pin_closed → rc 0 when the frontmatter has a closing --- +_effort_pin_closed() { + awk 'NR==1&&/^---$/{p=1;next} p&&/^---$/{f=1;exit} END{exit !f}' "$1" +} + +# _effort_pin_write — replace the frontmatter +# `effort:` line, or insert one after `name: ` (before the closing +# `---` when the frontmatter has no name line). Body lines never change. +# Writes a mktemp sibling then renames; any failure leaves no temp behind. +_effort_pin_write() { + local file="$1" name="$2" level="$3" tmp + tmp="$(mktemp "$file.XXXXXX")" || return 1 + cp -p "$file" "$tmp" && awk -v n="$name" -v lvl="$level" ' + NR==1 && /^---$/ { fm=1; print; next } + fm && /^---$/ { + if (!done) { print "effort: " lvl; done=1 } + fm=0; print; next + } + fm && /^effort: / { if (!done) { print "effort: " lvl; done=1 }; next } + fm && $0 == "name: " n { print; if (!done) { print "effort: " lvl; done=1 }; next } + { print } + ' "$file" > "$tmp" && mv "$tmp" "$file" && return 0 + rm -f "$tmp" + return 1 +} + +# _effort_pin_apply_one → rc 0 applied, 2 already at +# level, 1 failed (err line printed, file untouched or write rolled back) +_effort_pin_apply_one() { + local file="$1" name="$2" level="$3" + if ! _effort_pin_closed "$file"; then + err "effort-pins: $file: frontmatter never closed — skipped"; return 1 + fi + [ "$(_effort_pin_current "$file")" = "$level" ] && return 2 + if ! _effort_pin_write "$file" "$name" "$level"; then + err "effort-pins: $file: write failed"; return 1 + fi + if [ "$(_effort_pin_current "$file")" != "$level" ]; then + err "effort-pins: $file: level not applied (CRLF or malformed frontmatter?)" + return 1 + fi + return 0 +} + +# apply_effort_pins [repo-root] — walk the map, pin every vendored skill +apply_effort_pins() { + local repo="${1:-$EFFORT_PINS_REPO}" map name level rest file rc + local applied=0 kept=0 rejected=0 failed=0 + map="$repo/lib/effort-pins.txt" + [ -f "$map" ] || { err "effort-pins: map missing: $map"; return 1; } + while read -r name level rest || [ -n "$name" ]; do + case "$name" in ''|'#'*) continue ;; esac + if [ -n "$rest" ] || ! [[ "$name" =~ $EFFORT_PIN_NAME_RE ]] \ + || ! [[ "$level" =~ $EFFORT_PIN_LEVEL_RE ]]; then + err "effort-pins: rejected map line '$name $level $rest'" + rejected=$((rejected + 1)); continue + fi + file="$repo/skills-external/$name/SKILL.md" + [ -f "$file" ] || continue + _effort_pin_apply_one "$file" "$name" "$level"; rc=$? + case "$rc" in + 0) applied=$((applied + 1)) ;; + 2) kept=$((kept + 1)) ;; + *) failed=$((failed + 1)) ;; + esac + done < "$map" + ok "effort-pins: $applied applied, $kept already at level, $failed failed" + [ "$rejected" -eq 0 ] && [ "$failed" -eq 0 ] +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + apply_effort_pins "$@" +fi diff --git a/lib/effort-pins.txt b/lib/effort-pins.txt new file mode 100644 index 0000000..00b90dd --- /dev/null +++ b/lib/effort-pins.txt @@ -0,0 +1,46 @@ +# lib/effort-pins.txt — entry effort level of the vendored skills +# (skills-external//SKILL.md). Upstream copies carry no `effort:` and +# every resync rewrites SKILL.md, so the pin lives here and +# lib/effort-pins.sh re-applies it after the last vendoring step of +# install-plugins.sh and update-all.sh. One line = ` `, +# level in low|medium|high|xhigh|max. The census +# lib/tests/effort-routing.test.sh checks every vendored file against this +# map. Rungs (BDR-107, BDR-108): low = fix a line, run a script · medium = +# day-to-day · high = refactor, resisting bug · xhigh = architecture, audit +# before validation · max = stuck. +# +# superpowers (obra/superpowers, plugins.lock.json "superpowers") +brainstorming xhigh +writing-plans xhigh +requesting-code-review xhigh +subagent-driven-development high +writing-skills high +test-driven-development medium +using-git-worktrees low +# +# agent-skills (addyosmani/agent-skills, plugins.lock.json "agent-skills") +deprecation-and-migration high +ci-cd-and-automation medium +observability-and-instrumentation medium +# +# design stack — ONE level for every member: these skills load stacked in a +# single UI build and the last loaded wins (lib/effort-shift.md), so two +# levels in the stack would make the effort depend on load order. +# skills/site-motion (repo-authored) pins the same level in its frontmatter. +frontend-design high +emil-design-eng high +design-motion-principles high +21st-ui-build high +scroll-world-storytelling high +build-threejs-scroll-worlds high +scroll-scrubbed-visual-sequence high +scroll-scrubbed-word-reveal high +scroll-progress-timeline high +# +# 21st pack (`21st skills install`): tooling low, generation high, critique xhigh +21st-cli-use low +21st-registry low +21st-design-sync low +21st-ai high +21st-ui-explore high +21st-ui-review xhigh diff --git a/lib/effort-shift.md b/lib/effort-shift.md index 38b7ad7..fe92b1a 100644 --- a/lib/effort-shift.md +++ b/lib/effort-shift.md @@ -24,6 +24,11 @@ max (stuck error, judged need). Claude loads alone, such as `brainstorming` or `writing-plans`, applies nothing). Last loaded wins, both directions. The prompt cache survives a shift. +- **Stacked skills share one level**: skills that load together in one + build (the design stack) all pin the same level, since the last loaded + wins. Vendored externals get their level from `lib/effort-pins.txt`, + re-applied by `lib/effort-pins.sh` after every vendoring step; repo + skills carry it in their frontmatter. - Dispatched agents run on their own `effort:` pin, never on a shift. Unpinned agents inherit the level in force at dispatch. - Headless sessions (`-p`, `claude agents`, SDK) ignore skill-level effort: diff --git a/lib/tests/effort-pins.test.sh b/lib/tests/effort-pins.test.sh new file mode 100755 index 0000000..256ce7a --- /dev/null +++ b/lib/tests/effort-pins.test.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# lib/tests/effort-pins.test.sh — lib/effort-pins.sh's apply_effort_pins(): +# insert after `name:`, keep an equal level untouched, replace a different +# level inside the frontmatter only (a prose `effort:` in the body stays), +# skip a skill not vendored, insert before the closing `---` when the +# frontmatter has no name line, run idempotently, reject a bad level, a +# traversal name and a three-field line before writing anything, and +# parse the real map without error; hardening: last map line without a +# newline, unterminated frontmatter, CRLF file and read-only directory. All on a throwaway fixture repo. +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +LIB="$ROOT/lib/effort-pins.sh" +pass=0; fail=0 +check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); echo "PASS $1" + else fail=$((fail+1)); echo "FAIL $1: got[$2] want[$3]"; fi; } +fm_effort() { awk 'NR==1&&/^---$/{p=1;next} p&&/^---$/{exit} p' "$1" \ + | sed -n 's/^effort: //p' | head -1; } + +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT +REPO="$WORK/repo"; EXT="$REPO/skills-external" +mkdir -p "$REPO/lib" "$EXT/alpha" "$EXT/beta" "$EXT/gamma" "$EXT/noname" +printf -- '---\nname: alpha\ndescription: a\n---\nbody\n' > "$EXT/alpha/SKILL.md" +printf -- '---\nname: beta\neffort: low\n---\nprose says effort: max here\n' > "$EXT/beta/SKILL.md" +printf -- '---\nname: gamma\neffort: low\n---\nbody\n' > "$EXT/gamma/SKILL.md" +printf -- '---\ndescription: no name line\n---\nbody\n' > "$EXT/noname/SKILL.md" +printf '# map\nalpha high\nbeta medium\ngamma low\nghost xhigh\nnoname low\n' > "$REPO/lib/effort-pins.txt" +gamma_before="$(cat "$EXT/gamma/SKILL.md")" + +bash "$LIB" "$REPO" >/dev/null 2>&1; check T1-rc-clean "$?" 0 +check T2-insert-after-name "$(sed -n '3p' "$EXT/alpha/SKILL.md")" "effort: high" +check T3-replace-in-frontmatter "$(fm_effort "$EXT/beta/SKILL.md")" "medium" +check T3b-body-prose-untouched "$(grep -c 'effort: max' "$EXT/beta/SKILL.md")" 1 +check T3c-single-effort-line "$(grep -c '^effort:' "$EXT/beta/SKILL.md")" 1 +check T4-equal-level-untouched "$(cat "$EXT/gamma/SKILL.md")" "$gamma_before" +check T5-missing-skill-skipped "$([ -e "$EXT/ghost" ] && echo created || echo absent)" absent +check T6-no-name-inserts-before-closing "$(sed -n '3p' "$EXT/noname/SKILL.md")" "effort: low" +check T6b-no-name-still-frontmatter "$(fm_effort "$EXT/noname/SKILL.md")" "low" +snap="$(cat "$EXT"/*/SKILL.md)" +bash "$LIB" "$REPO" >/dev/null 2>&1 +check T7-idempotent "$(cat "$EXT"/*/SKILL.md)" "$snap" +check T7b-no-tmp-left "$(find "$EXT" -name '*.tmp' | wc -l)" 0 + +# rejections: nothing written, rc 1 +for bad in 'alpha turbo' '../evil high' 'alpha high extra'; do + printf '%s\n' "$bad" > "$REPO/lib/effort-pins.txt" + out="$(bash "$LIB" "$REPO" 2>&1)"; rc=$? + check "T8-rejected[$bad]-rc" "$rc" 1 + check "T8-rejected[$bad]-named" "$(printf '%s' "$out" | grep -c 'rejected map line')" 1 +done +check T8b-tree-unchanged-after-rejections "$(cat "$EXT"/*/SKILL.md)" "$snap" +check T8c-no-evil-dir "$([ -e "$WORK/evil" ] && echo created || echo absent)" absent + +# the real map parses: fixture repo with the real map and no vendored skill +mkdir -p "$WORK/real/lib" "$WORK/real/skills-external" +cp "$ROOT/lib/effort-pins.txt" "$WORK/real/lib/" +out="$(bash "$LIB" "$WORK/real" 2>&1)"; check T9-real-map-parses "$?" 0 +check T9b-real-map-nothing-applied "$(printf '%s' "$out" | grep -c '0 applied, 0 already')" 1 +check T10-missing-map-rc "$(bash "$LIB" "$WORK/nowhere" >/dev/null 2>&1; echo $?)" 1 + +# hardening: each case in its own fixture repo +mkrepo() { R="$WORK/$1"; mkdir -p "$R/lib" "$R/skills-external/$2"; } +mkrepo h11 alpha; mkdir "$WORK/h11/skills-external/beta" +printf -- '---\nname: alpha\n---\nb\n' > "$WORK/h11/skills-external/alpha/SKILL.md" +printf -- '---\nname: beta\n---\nb\n' > "$WORK/h11/skills-external/beta/SKILL.md" +printf 'alpha high\nbeta low' > "$WORK/h11/lib/effort-pins.txt" +bash "$LIB" "$WORK/h11" >/dev/null 2>&1 +check T11-last-line-no-newline "$(fm_effort "$WORK/h11/skills-external/beta/SKILL.md")" low + +mkrepo h12 open; f12="$WORK/h12/skills-external/open/SKILL.md" +printf -- '---\nname: open\nbody effort: max\n' > "$f12"; b12="$(cat "$f12")" +printf 'open high\n' > "$WORK/h12/lib/effort-pins.txt" +out="$(bash "$LIB" "$WORK/h12" 2>&1)"; rc=$? +check T12-unterminated-frontmatter-skipped \ + "$rc|$(cat "$f12" | cmp -s - <(printf '%s\n' "$b12") && echo same)|$(printf '%s' "$out" | grep -c "ERR .*$f12")" "1|same|1" + +mkrepo h13 crlf +printf -- '---\r\nname: crlf\r\n---\r\nbody\r\n' > "$WORK/h13/skills-external/crlf/SKILL.md" +printf 'crlf high\n' > "$WORK/h13/lib/effort-pins.txt" +out="$(bash "$LIB" "$WORK/h13" 2>&1)"; rc=$? +check T13-crlf-not-counted-applied \ + "$rc|$(printf '%s' "$out" | grep -c 'ERR ')|$(printf '%s' "$out" | grep -c ' 0 applied, ')" "1|1|1" + +mkrepo h14 ro; d14="$WORK/h14/skills-external/ro" +printf -- '---\nname: ro\n---\nb\n' > "$d14/SKILL.md" +printf 'ro high\n' > "$WORK/h14/lib/effort-pins.txt" +chmod 555 "$d14"; out="$(bash "$LIB" "$WORK/h14" 2>&1)"; rc=$?; chmod 755 "$d14" +check T14-write-failure-no-temp \ + "$rc|$(printf '%s' "$out" | grep -c 'ERR ')|$(find "$d14" -name 'SKILL.md.*' | wc -l)" "1|1|0" + +echo "effort-pins: $pass pass, $fail fail" +[ "$fail" -eq 0 ] diff --git a/lib/tests/effort-routing.test.sh b/lib/tests/effort-routing.test.sh index a6e4003..e6edeed 100755 --- a/lib/tests/effort-routing.test.sh +++ b/lib/tests/effort-routing.test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # lib/tests/effort-routing.test.sh — census: effort tiering (BDR-107) # agent pins, skill entry levels, shifter skills, orchestrator wiring, settings. -# shellcheck disable=SC2015 # A && ok || ko is deliberate here: ok/ko never fail, so C never masks a true A +# shellcheck disable=SC2015,SC2016 # A && ok || ko is deliberate (ok/ko never fail); '$REPO' locks are literal source text set -u R="$(cd "$(dirname "$0")/../.." && pwd)" pass=0; fail=0 @@ -46,15 +46,37 @@ for s in status commit-change release-candidate doc capitalize close reconcile d for s in gitflow prune-memory; do fm_has_effort "skills/$s/SKILL.md" medium; done for s in feat hotfix bugfix refactor web-validate harden seo geo; do fm_has_effort "skills/$s/SKILL.md" high; done for s in ship-feature init-project onboard tour audit-delta analyze code-clean client-handover; do fm_has_effort "skills/$s/SKILL.md" xhigh; done +# BDR-108 round: the three repo skills that had no level +fm_has_effort "skills/skills-perso/SKILL.md" low +fm_has_effort "skills/pdf-translate/SKILL.md" medium +fm_has_effort "skills/site-motion/SKILL.md" high -# ── 9) vendored superpowers carry xhigh (spec D3). The files live in skills-external/ (gitignored, -# machine-owned), so the durable artifact is the install-plugins.sh re-apply; the frontmatter -# check skips VISIBLY when the skill is not vendored yet (fresh clone before make plugin). -for s in brainstorming writing-plans; do - if [ -f "$R/skills-external/$s/SKILL.md" ]; then fm_has_effort "skills-external/$s/SKILL.md" xhigh +# ── 9) vendored externals carry the level of lib/effort-pins.txt (BDR-108). The files live in +# skills-external/ (gitignored, machine-owned): the durable artifact is the map + the re-apply +# after the last vendoring step of install-plugins.sh AND update-all.sh; a skill not vendored +# yet SKIPs visibly (fresh clone before make plugin). +while read -r s lvl _; do + case "$s" in ''|'#'*) continue ;; esac + if [ -f "$R/skills-external/$s/SKILL.md" ]; then fm_has_effort "skills-external/$s/SKILL.md" "$lvl" else printf 'SKIP skills-external/%s/SKILL.md not vendored yet (run make plugin)\n' "$s"; fi -done -has "install-plugins.sh" 'effort: xhigh' +done < "$R/lib/effort-pins.txt" +has "lib/effort-pins.txt" 'brainstorming xhigh'; has "lib/effort-pins.txt" 'writing-plans xhigh' +has "install-plugins.sh" 'apply_effort_pins "$REPO"'; has "update-all.sh" 'apply_effort_pins "$REPO"' +lacks "install-plugins.sh" 'for _s in brainstorming writing-plans; do' +ln_last() { grep -n "$2" "$R/$1" | tail -1 | cut -d: -f1; } +[ "$(ln_last install-plugins.sh 'apply_effort_pins "$REPO"')" -gt "$(ln_last install-plugins.sh 'rm -rf "$TFD_STAGE"')" ] \ + && ok || ko "install-plugins.sh: effort pins must be re-applied after the 21st pack refresh" +pins_ln=$(ln_last update-all.sh 'apply_effort_pins "$REPO"') +[ "$pins_ln" -gt "$(ln_last update-all.sh 'skills-external/$_tfd_name')" ] \ + && [ "$pins_ln" -gt "$(ln_last update-all.sh 'vendor_pinned_skills superpowers refresh')" ] \ + && ok || ko "update-all.sh: effort pins must be re-applied after the last vendoring step (21st pack)" +[ -x "$R/lib/effort-pins.sh" ] && ok || ko "lib/effort-pins.sh missing or not executable" +# 9b) design stack = ONE level (last loaded wins); site-motion (repo skill) pins the same one +stack_levels() { awk '/^# design stack/{f=1;next} f&&/^#$/{f=0} f&&!/^#/&&NF==2{print $2}' "$R/lib/effort-pins.txt" | sort -u; } +[ "$(stack_levels | wc -l)" -eq 1 ] && ok || ko "design stack must share ONE level in lib/effort-pins.txt (got: $(stack_levels | tr '\n' ' '))" +[ "$(stack_levels | wc -l)" -ge 1 ] && fm_has_effort "skills/site-motion/SKILL.md" "$(stack_levels | head -1)" +has "lib/effort-shift.md" 'Stacked skills share one level' +has "CLAUDE.global.md" 'lib/effort-pins.txt' # ── 5) shifter skills + include (spec D4) for l in low medium high xhigh max; do fm_has_effort "skills/effort-$l/SKILL.md" "$l"; has "skills/effort-$l/SKILL.md" "name: effort-$l"; done @@ -101,7 +123,7 @@ has "lib/effort-shift.md" 'Before any built-in or unpinned dispatch' has "lib/model-gate.md" 'built-ins inherit the effort in force' has "skills/ship-feature/SKILL.md" 'effort-shift: error recovery' for s in feat hotfix bugfix seo geo harden web-validate ship-feature init-project onboard code-clean audit-delta; do has "skills/$s/SKILL.md" 'effort-shift: own level before the challenge'; done -has "install-plugins.sh" 'for _s in brainstorming writing-plans; do' +has "update-all.sh" 'source "$REPO/lib/effort-pins.sh"' # ── summary (later tasks insert their locks ABOVE this line) printf 'effort-routing census: %d pass, %d fail\n' "$pass" "$fail" diff --git a/skills-external/design-motion-principles/SKILL.md b/skills-external/design-motion-principles/SKILL.md index e4cf1c7..684c757 100644 --- a/skills-external/design-motion-principles/SKILL.md +++ b/skills-external/design-motion-principles/SKILL.md @@ -1,5 +1,6 @@ --- name: design-motion-principles +effort: high description: "Motion and interaction design expert based on Emil Kowalski, Jakub Krehel, and Jhey Tompkins' techniques. Two modes — build interactive components with purposeful motion, or audit existing animations to catch AI-slop motion patterns (audit emits a branded HTML report with looping demos). Use when creating, adding, animating, or reviewing UI motion: transitions, hover states, micro-interactions, enter/exit animations, or any motion design work in React, Framer Motion, CSS, or HTML. Provides per-designer perspectives with context-aware weighting." --- diff --git a/skills/pdf-translate/SKILL.md b/skills/pdf-translate/SKILL.md index 44b5d7c..2d1cb59 100644 --- a/skills/pdf-translate/SKILL.md +++ b/skills/pdf-translate/SKILL.md @@ -1,5 +1,6 @@ --- name: pdf-translate +effort: medium description: Use when translating a PDF (especially OCR or image-based) to another language and producing faithful HTML output. Handles image extraction, layout preservation, contextual translation, and style-matched reconstruction. Triggers on "translate this PDF", "PDF en francais", "convert PDF to HTML translated", "traduire ce document". --- diff --git a/skills/site-motion/SKILL.md b/skills/site-motion/SKILL.md index caa80b3..3f2f7b8 100644 --- a/skills/site-motion/SKILL.md +++ b/skills/site-motion/SKILL.md @@ -1,5 +1,6 @@ --- name: site-motion +effort: high description: | Site-level motion choreography: scroll engine choice, page-transition rules, and pin/scrub sequencing across a whole page or Astro route — diff --git a/skills/skills-perso/SKILL.md b/skills/skills-perso/SKILL.md index 4283d45..dab68db 100644 --- a/skills/skills-perso/SKILL.md +++ b/skills/skills-perso/SKILL.md @@ -1,5 +1,6 @@ --- name: skills-perso +effort: low description: | List personal (user-created) skills from ~/.claude/skills/. Excludes framework/gstack skills and symlinked/external skills. diff --git a/update-all.sh b/update-all.sh index bc9f24f..0537837 100644 --- a/update-all.sh +++ b/update-all.sh @@ -513,6 +513,15 @@ print(d.get('21st',{}).get('version','latest')) rm -rf "$TFD_STAGE" fi +# Effort pins (BDR-107, BDR-108): every refresh above rewrites SKILL.md and +# drops the `effort:` line; the 21st pack refresh is the last step that rewrites +# a SKILL.md, so the entry levels of lib/effort-pins.txt go back here. +echo "" +echo "── Re-applying effort pins on the vendored skills..." +# shellcheck source=lib/effort-pins.sh disable=SC1091 +source "$REPO/lib/effort-pins.sh" +apply_effort_pins "$REPO" || warn "effort pins: map lines rejected — fix lib/effort-pins.txt" + # ── 7.5. Update external skills (npx skills) ── echo "" echo "── Updating external skills (npx skills)..."