super-ux 0.48.4 → 0.49.1

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,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.49.1 — the skills handoff refuses the shadow it used to create
4
+
5
+ - **The skills-menu item now consults the target home before delegating.** `npx skills add`
6
+ auto-detects Claude Code and writes a plain `~/.claude/skills/super-ux` copy even when
7
+ claude-code is never picked, and on a machine where super-ux is installed as a Claude
8
+ Code plugin that copy shadows the plugin and serves the version it was copied from
9
+ forever. The handoff now reads `~/.claude/plugins/installed_plugins.json` — the record of
10
+ what is actually installed, under any marketplace name — and refuses with **exit 3**, the
11
+ remedy in the refusal (`claude plugin marketplace update super-ux`, `claude plugin update
12
+ <spec from the JSON>`, and the family launcher), and `npx super-ux --force` as the named
13
+ override. The `plugins/marketplaces/super-ux` directory is kept only as the fallback
14
+ signal: it under-reports, because a directory-sourced marketplace has no dir there and
15
+ plugin names differ from marketplace names. A missing or corrupt JSON reads as "no
16
+ plugin" — fail open, never crash. Only the Claude Code channel is gated; `--cursor`
17
+ installs into a project beside the plugin exactly as before. Canon:
18
+ make-skill v0.25.0, `references/distribution.md` §3; reproduced live 2026-08-29 when a
19
+ bare `npx @ssheleg/telegram-dev` shipped three shadows past this exact class of hole.
20
+ - **A successful run now ends by saying how the next version arrives** — `npx
21
+ super-ux@latest` for this member, the family launcher for every channel at once. An
22
+ installer that never mentions updates has still chosen an update model: never.
23
+ - **`test/installer_test.js`** runs both installers against throwaway HOMEs — the
24
+ plugin-present refusal (exit 3, remedy, nothing delegated, nothing written), the
25
+ differently-named marketplace spec in the remedy, `--force`, corrupt-JSON fail-open, a
26
+ prefix collider (`super-ux-extra@x`), the marketplaces-dir fallback, the fresh HOME, and
27
+ that `install.sh` never touches `~/.claude` at all. Wired into `npm test` and CI; watched
28
+ failing against the pre-fix installer (7 of 10 cases red, the plugin-present case
29
+ delegating with exit 0) before the fix was un-stashed.
30
+ - SCN-016 and SCN-017 record the refusal and the update line in `docs/ux/scenarios.md`;
31
+ SCR-05 gains the `refused` state; the string registry carries the new copy and the
32
+ `refused:` prefix joins the state vocabulary.
33
+
34
+ ## 0.49.0 — the humanization pass names what it cannot prove
35
+
36
+ - **`ai-tells.md` now says these markers are not a verdict, and who they misjudge.** Every
37
+ marker is more common in model output; none proves a machine wrote anything, and the
38
+ failure is not symmetric. Independent audits found false-positive rates **above 60% on
39
+ non-native English writers** (Liang et al., Stanford, *Patterns*, 2023). Three rules bind
40
+ the rest of the file: never say a text was AI-written, only which markers appear at what
41
+ density; never gate on a marker count; and a second-language writer is not a defect to be
42
+ edited into fluency they did not ask for.
43
+ - **Other implementations are named, compared and pointed at** — `blader/humanizer` and
44
+ `conorbronsdon/avoid-ai-writing`, with a table of what each does that this pack does not
45
+ and the reverse. Reach outward for an audit that changes nothing (only one has a
46
+ detect-only mode), for long-form prose, or when the writer has a sample of their own
47
+ writing to match. Stay here for product copy: the brand pack's registers, terminology and
48
+ canonical facts are the constraint, and a general-purpose humanizer does not read them.
49
+ Two guards remain this pack's own — the 50% change-rate refusal and the mandatory
50
+ semantic-preservation check — and neither external implementation carries an equivalent.
51
+ - **`voice.md` gains an optional `Humanization pass:` field**, and its absence is
52
+ meaningful: it means nobody has been asked. `brand-voice` Init asks once and records the
53
+ answer; `copywriting` reads it rather than asking again. A value naming a tool that is not
54
+ installed falls back to `own` and says so — a missing optional tool must not stop copy
55
+ being written.
56
+ - `npx sshlg-skills humanizers` lists what is installed on the machine.
57
+
3
58
  ## 0.48.4 — the channel that sends the installs, on npm too
4
59
 
5
60
  - The `skills.sh` badge and the canonical `homepage` reached GitHub in the previous cycle and stopped
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.48.4",
3
+ "version": "0.49.1",
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": {
@@ -5,6 +5,7 @@ Locale parity threshold: 80%
5
5
  Derived-from: <P-NN, JTBD-NN from docs/ux/foundation.md — or `inferred`>
6
6
  Status: draft
7
7
  Last calibrated: <YYYY-MM-DD>
8
+ Humanization pass: <own | humanizer | avoid-ai-writing | none> # optional; absent = ask once
8
9
 
9
10
  # Voice
10
11