super-ux 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.50.0 — the templates ship where the texts say they are
4
+
5
+ - **`templates/` now travels with everything that names it** (SUX-01, family audit
6
+ 2026-08-29). Six shipped texts pointed at "the plugin's `templates/`" while the
7
+ marketplace ships `./plugins/super-ux` and the directory lived at the repo root only —
8
+ verified absent from all 13 cached installed versions, so `/brand-init`, `/brand`,
9
+ `/ux-rule` step 2 and the three seeding skills dead-ended at a path that resolves in
10
+ the one place users never run from: this repository's own checkout. The repo root stays
11
+ the single source (the hard rules and the installer CLI read it);
12
+ `test/sync_references.py` now mirrors the full tree into `plugins/super-ux/templates/`
13
+ and the named seeds into each seeding skill's own directory — a skill installed by the
14
+ skills CLI has no plugin root to reach up to — and the three skill texts say "this
15
+ skill's own `templates/…`" while the three command texts keep "the plugin's
16
+ `templates/…`", which is now true.
17
+ - **The class is gated, not just the instance.** `validate_shipped_templates` refuses a
18
+ copy that drifts from its source and a copy with no source, in both homes;
19
+ `validate_shipped_paths` requires every backticked `templates/…` or `scripts/…` token
20
+ in a shipped text to resolve inside what actually ships — the plugin root for commands,
21
+ the skill's own directory for skill files. Six defects planted, each caught by its own
22
+ branch with its own message, each reverted; the sync verified idempotent across three
23
+ runs. The validator grows 4111 → 4174 checks and `test/floors.json` ratchets with it.
24
+ - **`/ux-audit` admits its whole scope surface** (SUX-06). `copy` and
25
+ `benchmark:<competitor>` join the `argument-hint` and the step-1 enumeration — the body
26
+ has treated both as legal scopes since they shipped, while the two places an agent
27
+ reads first omitted them.
28
+ - **Trigger hygiene in three descriptions** (SUX-07, SUX-08, SUX-11). `ux-scenarios`
29
+ defers the empty-project start up the chain — vision and ux-foundation own it — instead
30
+ of claiming "ANY new feature or project" unqualified; `copywriting` narrows "build a
31
+ landing page / сделай лендинг" to the copy for it, naming sheleg-design as the visual
32
+ half; `ux-flows` drops the bare "figma"/"фигма" claim and delegates the visual system
33
+ and Figma variables to sheleg-design, mirroring its own body. Every phrase the family
34
+ umbrella routes on remains a literal substring of its description — verified with the
35
+ umbrella's own `advertised_check.js` (43/43, and watched failing against a dropped
36
+ «мокап»).
37
+
38
+ ## 0.49.1 — the skills handoff refuses the shadow it used to create
39
+
40
+ - **The skills-menu item now consults the target home before delegating.** `npx skills add`
41
+ auto-detects Claude Code and writes a plain `~/.claude/skills/super-ux` copy even when
42
+ claude-code is never picked, and on a machine where super-ux is installed as a Claude
43
+ Code plugin that copy shadows the plugin and serves the version it was copied from
44
+ forever. The handoff now reads `~/.claude/plugins/installed_plugins.json` — the record of
45
+ what is actually installed, under any marketplace name — and refuses with **exit 3**, the
46
+ remedy in the refusal (`claude plugin marketplace update super-ux`, `claude plugin update
47
+ <spec from the JSON>`, and the family launcher), and `npx super-ux --force` as the named
48
+ override. The `plugins/marketplaces/super-ux` directory is kept only as the fallback
49
+ signal: it under-reports, because a directory-sourced marketplace has no dir there and
50
+ plugin names differ from marketplace names. A missing or corrupt JSON reads as "no
51
+ plugin" — fail open, never crash. Only the Claude Code channel is gated; `--cursor`
52
+ installs into a project beside the plugin exactly as before. Canon:
53
+ make-skill v0.25.0, `references/distribution.md` §3; reproduced live 2026-08-29 when a
54
+ bare `npx @ssheleg/telegram-dev` shipped three shadows past this exact class of hole.
55
+ - **A successful run now ends by saying how the next version arrives** — `npx
56
+ super-ux@latest` for this member, the family launcher for every channel at once. An
57
+ installer that never mentions updates has still chosen an update model: never.
58
+ - **`test/installer_test.js`** runs both installers against throwaway HOMEs — the
59
+ plugin-present refusal (exit 3, remedy, nothing delegated, nothing written), the
60
+ differently-named marketplace spec in the remedy, `--force`, corrupt-JSON fail-open, a
61
+ prefix collider (`super-ux-extra@x`), the marketplaces-dir fallback, the fresh HOME, and
62
+ that `install.sh` never touches `~/.claude` at all. Wired into `npm test` and CI; watched
63
+ failing against the pre-fix installer (7 of 10 cases red, the plugin-present case
64
+ delegating with exit 0) before the fix was un-stashed.
65
+ - SCN-016 and SCN-017 record the refusal and the update line in `docs/ux/scenarios.md`;
66
+ SCR-05 gains the `refused` state; the string registry carries the new copy and the
67
+ `refused:` prefix joins the state vocabulary.
68
+
3
69
  ## 0.49.0 — the humanization pass names what it cannot prove
4
70
 
5
71
  - **`ai-tells.md` now says these markers are not a verdict, and who they misjudge.** Every
package/README.md CHANGED
@@ -248,7 +248,7 @@ to is a skill nobody runs.
248
248
  | `/vision` `/ux-init` `/ux-foundation` `/ux-flows` `/ux-update` `/ux-audit` `/ux-rule` `/ux-lint` `/ux-doctor` · `/brand` `/brand-init` `/brand-update` `/brand-lint` `/copy` | Direct controls for when you know exactly what you want; `/ux-rule` installs both hard rules and seeds `lint.py` + `doctor.py`; `/brand-init` seeds `docs/brand/` and its linter |
249
249
  | `docs/ux/lint.py` + `/ux-lint` | The deterministic half: missing Figma frames, unresolved SCR/story traces, orphans, built screens without coverage, index desync, ID gaps, broken links. Stdlib-only, exit 1 on problems, so wire it into CI and drift can't merge |
250
250
  | `cursor/rules/*.mdc` | The same methodology for Cursor: one always-on hard rule + seven agent-requested rules (vision, foundation, flows, scenarios, audit, brand voice, copywriting) |
251
- | `templates/` | Seeds for `docs/ux/`: the vision skeleton, foundation, flows, screens, scenario base, the folder README, and the audit-report skeleton. Both hard-rule snippets live here as their single source, `claude-rule.md` (scenario-first) and `vision-rule.md` (vision alignment), and the validator fails if a command's embedded copy drifts from them. Seeds for `docs/brand/`: voice, terminology, facts, channels, the string registry, a locale delta, and its folder README |
251
+ | `templates/` | Seeds for `docs/ux/`: the vision skeleton, foundation, flows, screens, scenario base, the folder README, and the audit-report skeleton. Both hard-rule snippets live here as their single source, `claude-rule.md` (scenario-first) and `vision-rule.md` (vision alignment), and the validator fails if a command's embedded copy drifts from them. Seeds for `docs/brand/`: voice, terminology, facts, channels, the string registry, a locale delta, and its folder README. The tree is mirrored into `plugins/super-ux/templates/` (and the seeds each skill names into that skill's own directory) by `test/sync_references.py`, so installed channels carry the seeds their texts point at; the validator refuses drift between the copies |
252
252
 
253
253
  The contracts every skill reads:
254
254
 
package/bin/super-ux.js CHANGED
@@ -7,16 +7,60 @@
7
7
  * rules into a project, Claude Code plugin user-globally. Non-TTY stdin gets
8
8
  * a text fallback ("1,3" / "all"). Flags keep the non-interactive paths:
9
9
  * --cursor [dir] [--force].
10
+ *
11
+ * The skills handoff is gated: while super-ux is installed as a Claude Code
12
+ * plugin, delegating to the skills CLI would recreate the plain
13
+ * ~/.claude/skills/super-ux copy that shadows the plugin, so the handoff is
14
+ * refused (exit 3) unless --force records the two-channel choice.
10
15
  */
11
16
  'use strict';
12
17
 
13
18
  const fs = require('fs');
19
+ const os = require('os');
14
20
  const path = require('path');
15
21
  const readline = require('readline');
16
22
  const { spawnSync } = require('child_process');
17
23
 
18
24
  const ROOT = path.resolve(__dirname, '..');
19
25
  const REPO = 'ssheleg/super-ux';
26
+ const NAME = 'super-ux';
27
+
28
+ // Exit codes are the contract: 0 installed or nothing selected, 1 error,
29
+ // 3 refused — the plugin channel owns this agent (--force overrides).
30
+ const EXIT_PLUGIN_PRESENT = 3;
31
+
32
+ /**
33
+ * The plugin spec (`<name>@<marketplace>`) installed for `name` in this home,
34
+ * or null.
35
+ *
36
+ * `installed_plugins.json` is the record of what is actually installed. The
37
+ * `plugins/marketplaces/<name>` directory under-reports: a marketplace added
38
+ * from a local `directory` source has no dir there at all, and plugin names
39
+ * differ from marketplace names, so a check keyed on it stays green while the
40
+ * shadow lands. Absence and corruption both read as "no plugin": the fresh
41
+ * HOME is the common case, and an installer that crashes on a parse error
42
+ * refuses the machines that need it most.
43
+ */
44
+ function installedPluginSpec(home, name) {
45
+ try {
46
+ const raw = fs.readFileSync(
47
+ path.join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8');
48
+ const parsed = JSON.parse(raw);
49
+ const plugins =
50
+ parsed && typeof parsed === 'object' &&
51
+ parsed.plugins && typeof parsed.plugins === 'object'
52
+ ? parsed.plugins
53
+ : parsed;
54
+ if (!plugins || typeof plugins !== 'object') return null;
55
+ for (const spec of Object.keys(plugins)) {
56
+ if (spec === name) return `${name}@${name}`;
57
+ if (spec.startsWith(name + '@')) return spec;
58
+ }
59
+ } catch {
60
+ // missing or corrupt = no plugin — fail open on absence, never crash
61
+ }
62
+ return null;
63
+ }
20
64
 
21
65
  const MENU_ITEMS = [
22
66
  { key: 'skills', label: 'Skills for any AI agent (Claude Code, Codex, Cursor, 70+; opens agent picker)' },
@@ -28,13 +72,20 @@ function usage() {
28
72
  console.log(`super-ux installer
29
73
 
30
74
  Usage:
31
- npx super-ux interactive menu (multi-select)
75
+ npx super-ux [--force] interactive menu (multi-select)
32
76
  npx super-ux --cursor [project-dir] [--force] Cursor rules, non-interactive
33
77
  npx super-ux --help
34
78
 
79
+ Exit codes:
80
+ 0 installed or nothing selected 1 error
81
+ 3 refused: the super-ux PLUGIN is installed in this home, and the skills
82
+ CLI would write the plain ~/.claude/skills copy that shadows it
83
+ (pass --force to run the picker anyway)
84
+
35
85
  Menu items (select any combination, 'a' = all):
36
86
  1. Skills for any AI agent (Claude Code, Codex, Cursor, 70+) — delegates to
37
87
  'npx skills add ${REPO}' with its agent/global/project picker.
88
+ Refused while the super-ux plugin is installed (see exit code 3).
38
89
  2. Cursor rules: cursor/rules/*.mdc -> <project>/.cursor/rules/, plus the
39
90
  docs/ux skeleton, the docs/brand pack, and all three linters
40
91
  (docs/ux/lint.py, docs/ux/doctor.py, docs/brand/lint.py). Existing
@@ -152,10 +203,62 @@ function run(cmd, args) {
152
203
  return result.status === 0 ? 'ok' : 'failed';
153
204
  }
154
205
 
155
- function installSkillsCli() {
206
+ /**
207
+ * One channel per agent. The skills CLI auto-detects Claude Code and writes
208
+ * ~/.claude/skills/super-ux even when claude-code is never picked, and on a
209
+ * machine where super-ux is installed as a Claude Code plugin that plain copy
210
+ * shadows the plugin and serves the version it was copied from forever. So
211
+ * the handoff consults the TARGET home's installed_plugins.json first and
212
+ * refuses LOUDLY: a refusal that exits 0 reads as success to every script
213
+ * above it. The marketplaces/<name> dir is only the fallback signal — a
214
+ * directory-sourced marketplace has no dir there, and plugin names differ
215
+ * from marketplace names. Reproduced live 2026-08-29: a bare
216
+ * `npx @ssheleg/telegram-dev` shipped three shadows past exactly this class
217
+ * of hole while the plugin was enabled.
218
+ */
219
+ function installSkillsCli(force) {
220
+ const home = os.homedir();
221
+ const spec = installedPluginSpec(home, NAME);
222
+ const marketplace = path.join(home, '.claude', 'plugins', 'marketplaces', NAME);
223
+ const viaMarketplaceDir = !spec && fs.existsSync(marketplace);
224
+ if ((spec || viaMarketplaceDir) && !force) {
225
+ const found = spec
226
+ ? `installed as the Claude Code plugin ${spec}\n` +
227
+ ' (declared in ~/.claude/plugins/installed_plugins.json)'
228
+ : `registered as a Claude Code marketplace\n (${marketplace})`;
229
+ console.error(
230
+ `refused: super-ux is already ${found}.\n` +
231
+ ' The skills CLI auto-detects Claude Code and would write a plain copy\n' +
232
+ ' to ~/.claude/skills/super-ux, which shadows the plugin and serves the\n' +
233
+ ' version it was copied from forever. Update the plugin channel instead:\n' +
234
+ ' claude plugin marketplace update super-ux\n' +
235
+ ` claude plugin update ${spec || 'super-ux@super-ux'}\n` +
236
+ ' Family launcher (updates every member, prunes shadow copies):\n' +
237
+ ' npx --yes sshlg-skills@latest update\n' +
238
+ ' Pass --force (npx super-ux --force) to run the picker anyway: a\n' +
239
+ ' deliberate choice to run two channels, where the stale one wins.'
240
+ );
241
+ return 'refused';
242
+ }
156
243
  console.log(`\n--- Skills for any agent: delegating to the skills CLI picker ---`);
157
244
  const status = run('npx', ['--yes', 'skills', 'add', REPO]);
158
245
  if (status !== 'ok') console.error(`warning: 'npx skills add ${REPO}' ${status}`);
246
+ return status;
247
+ }
248
+
249
+ /**
250
+ * The last line of a successful run says how the next version arrives —
251
+ * "Installed" is not a complete sentence. Auto-update is off on purpose:
252
+ * this member composes with its family, and per-marketplace autoUpdate moves
253
+ * each member on its own clock, into combinations nobody tested together.
254
+ */
255
+ function printUpdateLine() {
256
+ console.log(
257
+ '\nUpdates: rerun npx super-ux@latest (--cursor <dir> --force refreshes a\n' +
258
+ "project's rules and linters), or refresh the whole family with\n" +
259
+ 'npx --yes sshlg-skills@latest update (every channel, and it prunes plain\n' +
260
+ 'copies that would shadow a plugin).'
261
+ );
159
262
  }
160
263
 
161
264
  function installClaudePlugin() {
@@ -305,7 +408,7 @@ async function selectFallback(items, prompter) {
305
408
  return picked;
306
409
  }
307
410
 
308
- async function menu() {
411
+ async function menu(force) {
309
412
  console.log('super-ux: scenario-driven UI development. Select what to install:\n');
310
413
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
311
414
 
@@ -340,11 +443,21 @@ async function menu() {
340
443
 
341
444
  if (keys.includes('cursor')) installCursor(cursorDir, false);
342
445
  if (keys.includes('claude')) installClaudePlugin();
343
- if (keys.includes('skills')) installSkillsCli();
446
+ let refused = false;
447
+ if (keys.includes('skills')) refused = installSkillsCli(force) === 'refused';
344
448
 
345
449
  // Same offer the --cursor flag path makes. Two doors into one install that
346
450
  // behave differently is how a feature comes to exist for half its users.
451
+ // Offered on the refused path too: the skill IS present on this machine —
452
+ // as the plugin — so the routing block is exactly as wanted.
347
453
  offerRouters();
454
+ if (refused) {
455
+ // The refusal already carries the update commands; repeating the update
456
+ // line under it would bury the remedy. Exit 3 so scripts read the refusal.
457
+ process.exitCode = EXIT_PLUGIN_PRESENT;
458
+ } else {
459
+ printUpdateLine();
460
+ }
348
461
  }
349
462
 
350
463
  /**
@@ -379,7 +492,14 @@ function main() {
379
492
  return;
380
493
  }
381
494
  if (args.length === 0) {
382
- menu();
495
+ menu(false);
496
+ return;
497
+ }
498
+ // `--force` alone still opens the menu: it is the named override for the
499
+ // skills-handoff refusal, so it must be reachable from the same door the
500
+ // refusal names.
501
+ if (args.length === 1 && args[0] === '--force') {
502
+ menu(true);
383
503
  return;
384
504
  }
385
505
  if (args[0] !== '--cursor') {
@@ -391,6 +511,7 @@ function main() {
391
511
  const dirArg = args[1] && args[1] !== '--force' ? args[1] : '.';
392
512
  installCursor(path.resolve(dirArg), force);
393
513
  offerRouters();
514
+ printUpdateLine();
394
515
  }
395
516
 
396
517
  main();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "super-ux",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "scripts": {
5
- "test": "python3 test/validate.py && python3 test/brand_lint_test.py && python3 test/ux_lint_test.py && python3 docs/ux/lint.py && python3 docs/brand/lint.py"
5
+ "test": "python3 test/validate.py && python3 test/brand_lint_test.py && python3 test/ux_lint_test.py && python3 docs/ux/lint.py && python3 docs/brand/lint.py && node test/installer_test.js"
6
6
  },
7
7
  "description": "Scenario-driven UI development for AI agents (Claude Code, Cursor, 70+ agents): a versioned design chain in docs/ux/, a scenario-first hard rule, a deterministic drift linter, and evidence-backed UX audits. This package is the installer CLI.",
8
8
  "bin": {