feat(skills): act on stale CLI pin during project resume#2540
Conversation
The entry skill now keeps a resumed project's pinned CLI current instead of
leaving that to a notice nobody acts on. On resuming a project with pinned
scripts, run the read-only probe 'npx hyperframes@latest upgrade --project
. --check'; when it (or the stale-pin stderr notice, or _meta.updateAvailable
from a pinned run) reports the project behind, apply the bump and verify
with 'hyperframes check'. A failed check reverts the bump and keeps the
project on its pinned version, preserving the reproducibility contract the
pin exists for.
The probe matters because the stale-pin notice only exists in >= 0.7.59:
a pinned run of an older CLI prints no warning at all, so a notice-only
trigger never fires for exactly the projects most behind. The probe runs
unpinned, so its behavior never depends on the project's CLI age.
Telemetry: the fleet converges to new releases within about a week via the
background auto-updater and ephemeral npx, but pinned projects form a
persistent stale tail (~10% of weekly actives, e.g. 6.3k users still on
0.6.x three weeks after 0.7.0).
Both skill surfaces now pass an explicit dir ('--project .') because a bare
'--project' followed by another flag consumes that flag as its directory
value and no-ops; the parsing fix is a separate CLI change.
74ff09f to
7eff755
Compare
citty parses --project as a string option, so 'upgrade --project --check'
arrived with project="--check": the dir resolved to a nonexistent path and
the command no-opd with 'No package.json found' while --check was lost.
The documented default-cwd behavior only worked when --project was the
final token — and the trap-prone form is exactly what the scaffolded
template CLAUDE.md instructs.
A leading dash can never be a real directory argument, so resolveProjectArgs
now reclaims the eaten token as the flag the user wrote (--check / --json),
falls back to the current directory, and drops unrelated eaten flags rather
than treating them as paths. Templates and skill references switch to the
explicit-dir form ('--project .'), which behaves correctly on every release
including ones that predate this fix.
miga-heygen
left a comment
There was a problem hiding this comment.
Review — feat(skills): act on stale CLI pin during project resume
Scope: 7 files, +88/−10 — arg-parsing fix for upgrade --project, new skill rule, doc alignment.
SSOT inventory
| Artifact | Role | Verdict |
|---|---|---|
resolveProjectArgs |
Canonical resolver for the eaten-flag trap — reclaims --check/--json, drops unknowns, defaults dir to . |
Clean — pure function, single consumer, no duplicated decision |
| SKILL.md § "Keep the project's CLI current" | Agent-facing instruction: probe → apply → verify → revert-on-fail | Clean — actionable, covers failure path, acts-don't-relay |
| Template docs (CLAUDE.md + AGENTS.md) | Both switch --project → --project . |
Clean — byte-identical by design (different AI-tool targets) |
upgrade-info-misc.md |
Adds backward-compat warning for older releases | Clean — matches the fix |
What I checked
-
Eaten-flag matrix.
--project --check,--project --json,--project --check --json(in both orderings),--project(bare),--project ""— all resolve correctly through the reclaim/boolean/empty branches. The--yescase (unrelated to project mode) correctly drops todir: "."without reclaim, which is a no-op since--yeshas no meaning in the--projectpath. -
Caller integration. The command handler now threads
p.checkandp.json(fromresolveProjectArgs) instead ofcheckOnlyanduseJson(from rawargs). This is the crux of the fix: if--checkwas eaten,args.check === falsebutp.check === true. Single source of truth for all three values in the project branch. -
Test coverage. 4 cases covering reclaim, bare/boolean, real directory, unrelated flag. Each asserts the full
{dir, check, json}triple. -
Skill instruction quality. Read-only probe → apply →
hyperframes check→ revert-on-fail is the right shape. The "keep the explicit." rationale (backward compat with older CLI releases) is a good detail. "Act on the signal rather than relaying it to the user" is exactly right for an agent skill.
Edge case noted (not blocking)
A directory name starting with - (e.g., -my-weird-dir) would be treated as an eaten flag and silently default to .. Pathological in practice — Unix directories starting with - are adversarial even in normal shell usage — but worth a comment if anyone ever wonders why resolveProjectArgs("-foo", ...) returns ..
Verdict
Clean PR. Arg-parsing fix is correct, well-tested, and the skill/doc surfaces are all aligned to the explicit-dir form. No SSOT violations.
— Miga
jrusso1020
left a comment
There was a problem hiding this comment.
Independent review, additive to @miga. Clean, well-scoped PR — the revert-if-broken safety is exactly the right call for the 6k-on-0.6.x cohort.
✅ Mechanism sound + safe. The template / SKILL instructions have the agent probe read-only (upgrade --project . --check), apply only when behind, verify with hyperframes check, and — the key safety — revert the package.json + stay pinned + report if the check fails. "Act on the signal, never leave a bumped pin unverified" is the right posture. resolveProjectArgs correctly reclaims the citty --project --check arg-eating quirk, and the unknown-flag / --yes → cwd edge is covered by a test.
One consideration (non-blocking): hyperframes check validates composition structure/lint, not render-output equivalence — so a bump that passes check can still change a project's rendered output. The pin exists so a project "re-renders identically over time" (your own template copy); auto-advancing it trades that reproducibility for the newer fixes. Defensible as a default (most stale users want the fixes, and the revert covers hard breaks) — but a check-passing bump isn't guaranteed byte-identical output, so for a reproducibility-sensitive project a silent advance could surprise. Worth considering: have the agent note the bump it made to the user, or a --check line that flags "output may shift," rather than a fully silent rewrite.
The un-reachable tail (users who never refresh skills at all) is real but genuinely outside what the CLI side can reach — that's the comms question, not a code one.
— Rames Jusso
Review follow-up on the stale-pin rule: 'hyperframes check' validates composition structure, not render-output equivalence, so a check-passing bump can still shift a project's rendered output. The bump stays the right default for stale projects, but it must not be silent — the summary now names the old and new version so the user knows the reproducibility trade was made.
jrusso1020
left a comment
There was a problem hiding this comment.
Approving at 1e6e4f0 on James's go.
The new commit since my review (1e6e4f0, "report a successful pin bump in the run summary") resolves the one consideration I raised — the SKILL.md now states plainly that a passing check "confirms the project's compositions still validate on the new version — not that rendered output is frame-identical to the old pin — so a successful bump is never silent: name the old and new version in the run's summary." That's exactly the fix: acknowledges check ≠ output-equivalence and requires the agent to surface the bump instead of a silent rewrite.
Re-verified the incremental diff (061bcbf→1e6e4f0): 2-line doc change + expected skills-manifest hash auto-regen — zero render impact. The mechanism (probe → apply-only-if-behind → verify → revert-on-fail) and the resolveProjectArgs citty-quirk fix are unchanged and sound.
CI green except two regression-shards still pending — doc-only diff, so no render exposure, and the merge stays CI-gated by branch protection regardless.
— Rames Jusso
Why
Telemetry shows the CLI fleet converges to new releases within about a week — the background auto-updater covers global installs and ephemeral
npx @latestruns are always fresh. The persistent stale tail (~10% of weekly actives; 6.3k users were still on 0.6.x three weeks after 0.7.0 shipped) is almost entirely projects with pinned scripts:initwriteshyperframes@<version>intopackage.jsonscripts for render reproducibility, and nothing advances that pin.A notice-only trigger can't reach that tail: the stale-pin stderr notice shipped in 0.7.59, and a pinned run of an older CLI prints no warning at all — so the projects most behind are exactly the ones that emit no signal.
What
1. Entry-skill rule (
skills/hyperframes/SKILL.md§ 1). When resuming a project whose scripts carry a pin, probe once (read-only, runs the latest CLI regardless of the project's pin):npx hyperframes@latest upgrade --project . --checkWhen the probe — or the stale-pin notice, or
_meta.updateAvailable: truefrom a pinned-script run — reports the project behind, apply withupgrade --project .and verify withnpx hyperframes check. A failed check reverts the bump and keeps the project on its pinned version, preserving the reproducibility contract the pin exists for. The signal is acted on, not relayed.2.
--projectarg-parsing fix (found while testing). citty parses--projectas a string option, soupgrade --project --checkconsumed--checkas the directory value and no-opd withNo package.json found— the documented default-cwd behavior only worked with--projectas the final token, and the trap-prone form is exactly what the scaffolded template CLAUDE.md instructed.resolveProjectArgsnow reclaims an eaten--check/--json, defaults to the current directory, and drops unrelated eaten flags rather than treating them as paths. Unit-tested.3. Doc surfaces aligned. Scaffolded templates (
_shared/CLAUDE.md+AGENTS.md, kept byte-identical) andhyperframes-cli/references/upgrade-info-misc.mdswitch to the explicit-dir form--project ., which behaves correctly on every release including ones that predate the parsing fix.Tested
Against published
hyperframes@0.7.60on a scratch project pinning0.6.121in three scripts:upgrade --project . --check→would bump project scripts 0.6.121 → 0.7.60, package.json untouched... --check --json→{ changed: true, from: ["0.6.121"], to: "0.7.60", path: ... }upgrade --project .→ all three pins rewritten to0.7.60already on hyperframes@0.7.60)upgrade --project --check(bare) → reproduces the trap on 0.7.60; with this branch's source the same invocation reports the delta correctly and leaves package.json untouchedupgrade.project.test.tscovers the reclaim (--check/--json), bare/boolean--project, real-directory pass-through, and unrelated eaten flags. 8/8 pass; oxlint and oxfmt clean.Why the skill channel
Skills propagate from
maindirectly (skills add --full-depth/skills update), so the rule reaches users without waiting for a CLI release — and because probe and bump are invoked unpinned, they work even when the project's installed CLI predates this change.npx @latestruns