spine-rigc 1.0.2 → 1.1.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/README.md CHANGED
@@ -146,8 +146,9 @@ to do with it, and it is the one instrument here that can see a wrong animation.
146
146
 
147
147
  The guides under [Documentation](#documentation) also ship as
148
148
  [Agent Skills](https://agentskills.io) — `skills/<name>/SKILL.md`, in this
149
- repository and in the npm package — so an agent finds rigc the way it finds its
150
- other tools. Each skill is a router and nothing more: when to load it, the
149
+ repository and in the npm package — which a host reads once they are where it
150
+ looks: Claude Code through the plugin below, Codex, Gemini CLI and Antigravity
151
+ through `rigc skills install`. Each skill is a router and nothing more: when to load it, the
151
152
  non-negotiables in a line apiece, and a link to the guide that owns every rule, so
152
153
  a rule keeps living in exactly one place. The repository is also a Claude Code
153
154
  plugin marketplace:
@@ -162,6 +163,35 @@ loads the same skills without a marketplace. The plugin carries no version of it
162
163
  own — `/plugin update` follows `main` commit by commit, and the only version on
163
164
  disk stays the one in `package.json`.
164
165
 
166
+ Codex, Gemini CLI and Antigravity read skills from one directory in the workspace,
167
+ `.agents/skills/` ([Codex](https://learn.chatgpt.com/docs/build-skills),
168
+ [Gemini CLI](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/skills.md),
169
+ [Antigravity](https://antigravity.google/docs/skills/)), and none of them reads
170
+ `node_modules`. With the package installed, one command puts every skill there:
171
+
172
+ ```shell
173
+ bun add -d spine-rigc
174
+ bun rigc skills install # relative links: .agents/skills/rigc -> ../../node_modules/spine-rigc/skills/rigc
175
+ bun rigc skills install --copy # the folders themselves, for a host that does not follow a link
176
+ ```
177
+
178
+ Run it through the project's own install, as above: `bunx spine-rigc skills install`
179
+ in a project that has the package was measured running the registry's copy instead
180
+ of the project's. A link reaches every upgrade of the package with no second run,
181
+ and a second run has nothing to do; an entry already there that this command did not
182
+ make is refused by name and nothing is written. Gemini CLI 0.41.1 was measured
183
+ listing a linked skill from both its workspace and its user directory — the
184
+ workspace one only in a folder it trusts. Codex's documentation says it follows a
185
+ symlinked skill folder, which is not measured here, and Antigravity CLI 1.1.9 has no
186
+ way to list skills without starting a session, so what it does with a link is not
187
+ measured either; `--copy` is the shape that asks nothing of a host. Gemini CLI can
188
+ also fetch a skill itself, one folder at a time:
189
+ `gemini skills install https://github.com/firejune/rigc.git --path skills/rigc --scope workspace`.
190
+ The routers are named `rigc-rigging`, `rigc-motion`, `rigc-face` and `rigc-ingest`
191
+ because that directory is flat — beside another tool's `motion`, a bare name is
192
+ whichever one the host picked — and under the Claude Code plugin they read
193
+ `rigc:rigc-motion` and so on.
194
+
165
195
  ## First rig in ten minutes
166
196
 
167
197
  A whole rig, end to end, in a scratch directory: three tiny plates, two JSON
@@ -511,6 +541,7 @@ commands take it and what its default is.
511
541
  | `bonedist --candidate … --reference … --bones …` | per-frame, per-bone world-transform distance against another skeleton — the ladder's stage 3, run on its own. `--bones <correspondence.json \| identity>` is required rather than defaulted: a candidate is entitled to its own bone names, so the pairing is stated |
512
542
  | `check --candidate <dir> --frames <dir>` | the candidate against reference pictures — the only instrument here that can see a *wrong animation* |
513
543
  | `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
544
+ | `skills install [--dir …] [--copy]` | links every agent skill the package ships into `.agents/skills` (or `--dir`) as relative symlinks, or copies them with `--copy` — where Codex, Gemini CLI and Antigravity look. A second run has nothing to do; an entry already there that is not this command's is refused by name and nothing is written |
514
545
 
515
546
  `diff`, `bonedist`, `check` and `bench` measure against something you were given; the
516
547
  first three work on any reference you have, and `bench` is a repository workflow that needs a clone
package/cli.ts CHANGED
@@ -31,8 +31,22 @@
31
31
  * Its paths resolve against the cuts.json file itself, so the table travels
32
32
  * with the project that owns the art rather than with this repository.
33
33
  */
34
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
35
- import { basename, dirname, join, resolve } from 'node:path';
34
+ import {
35
+ appendFileSync,
36
+ cpSync,
37
+ existsSync,
38
+ lstatSync,
39
+ mkdirSync,
40
+ readdirSync,
41
+ readFileSync,
42
+ readlinkSync,
43
+ realpathSync,
44
+ rmSync,
45
+ statSync,
46
+ symlinkSync,
47
+ writeFileSync,
48
+ } from 'node:fs';
49
+ import { basename, dirname, join, relative, resolve } from 'node:path';
36
50
  import {
37
51
  BallotError,
38
52
  buildBallot,
@@ -229,7 +243,7 @@ function repositoryUrl(): string {
229
243
  * needs a value` (issue #328). `CLI10`/`CLI11` in `selftest.ts` now hold the two
230
244
  * halves together by reading `--help` rather than by naming a flag.
231
245
  */
232
- const BOOLEAN_FLAGS = new Set(['all-frames', 'all-bones', 'help', 'copy-images', 'again', 'pack']);
246
+ const BOOLEAN_FLAGS = new Set(['all-frames', 'all-bones', 'help', 'copy-images', 'again', 'pack', 'copy']);
233
247
 
234
248
  /**
235
249
  * The flags a command is allowed to spell more than once.
@@ -3270,6 +3284,223 @@ function cmdIngest(flags: Record<string, string>, positional: string[]): void {
3270
3284
  }
3271
3285
  }
3272
3286
 
3287
+ // ---------------------------------------------------------------------------
3288
+ // skills install — put the shipped skills where an agent host looks (issue #831)
3289
+ // ---------------------------------------------------------------------------
3290
+ //
3291
+ // After `bun add -d spine-rigc` the skills sit at `node_modules/spine-rigc/skills/`,
3292
+ // which no host reads. Codex, Gemini CLI and Antigravity all read
3293
+ // `<workspace>/.agents/skills/<name>/`, so this links every `skills/<name>/` the
3294
+ // package ships into one directory — `.agents/skills` under the working
3295
+ // directory unless `--dir` says otherwise.
3296
+ //
3297
+ // ⭐ The skills are found from THIS FILE's location, never from the working
3298
+ // directory: the command installs the package it is, and a cwd that happens to
3299
+ // hold some other `skills/` is not a source. That is also why it lives here and
3300
+ // not in `src/`: its one input is where the CLI was installed, nothing else
3301
+ // calls it, and `src/` is about rigs.
3302
+ //
3303
+ // A RELATIVE symlink by default, so the directory survives the project being
3304
+ // moved or cloned elsewhere and an upgrade of the package is seen with no second
3305
+ // run. `--copy` writes the folder instead, for a host that does not follow a
3306
+ // linked skill folder.
3307
+ //
3308
+ // 🔒 **An entry that is already there and is not what this command would write is
3309
+ // refused by name, and then nothing at all is written.** The check runs over
3310
+ // every skill before the first write, so a refusal never leaves half an install
3311
+ // behind. The one entry that is NOT refused is the one this command would have
3312
+ // made — a link that already resolves to the same skill folder, however it is
3313
+ // spelled, or with `--copy` a folder whose files are byte for byte the package's
3314
+ // — and a run over only those says it had nothing to do. No lifecycle script
3315
+ // does this on install: a postinstall writing into a consumer's project root is
3316
+ // refused as design, and Bun does not run a dependency's lifecycle scripts
3317
+ // outside `trustedDependencies`, so half the installs would silently skip it.
3318
+ // ---------------------------------------------------------------------------
3319
+
3320
+ /** The default `--dir`, resolved against the caller's working directory. */
3321
+ const DEFAULT_SKILLS_DIR = '.agents/skills';
3322
+
3323
+ /** What `rigc skills` offers. One today; the list is what the refusal of any other word prints. */
3324
+ const SKILLS_SUBCOMMANDS = ['install'];
3325
+
3326
+ /** An install refused before its first write — nothing to install, or an entry in the way. Exit 1. */
3327
+ class SkillsInstallError extends Error {}
3328
+
3329
+ type SkillsInstallAction = 'linked' | 'copied' | 'already linked' | 'already copied';
3330
+
3331
+ interface SkillsInstallEntry {
3332
+ /** `<dir>/<name>`. */
3333
+ target: string;
3334
+ /** `<package>/skills/<name>`. */
3335
+ source: string;
3336
+ action: SkillsInstallAction;
3337
+ /** The link text, relative to the directory it sits in, when the entry is a link. */
3338
+ link: string;
3339
+ }
3340
+
3341
+ /** Every `<name>/` under `source` that holds a `SKILL.md`, in name order, so two runs print the same lines. */
3342
+ function shippedSkills(source: string): string[] {
3343
+ if (!existsSync(source) || !statSync(source).isDirectory()) return [];
3344
+ return readdirSync(source)
3345
+ .filter((name) => statSync(join(source, name)).isDirectory() && existsSync(join(source, name, 'SKILL.md')))
3346
+ .sort();
3347
+ }
3348
+
3349
+ /**
3350
+ * The real path of `path` whether or not it exists yet: the real path of its
3351
+ * nearest existing ancestor with the rest appended. A relative link has to be
3352
+ * computed between two paths spelled the same way, and on macOS the temp
3353
+ * directory alone is reached as `/var/…` and is really `/private/var/…`.
3354
+ */
3355
+ function realpathAhead(path: string): string {
3356
+ const rest: string[] = [];
3357
+ let at = resolve(path);
3358
+ while (!existsSync(at)) {
3359
+ const up = dirname(at);
3360
+ if (up === at) break;
3361
+ rest.unshift(basename(at));
3362
+ at = up;
3363
+ }
3364
+ return join(realpathSync(at), ...rest);
3365
+ }
3366
+
3367
+ /** Every file under `root`, relative and sorted, so two trees compare in one order. */
3368
+ function filesUnder(root: string, prefix = ''): string[] {
3369
+ const out: string[] = [];
3370
+ for (const name of readdirSync(join(root, prefix)).sort()) {
3371
+ const rel = prefix === '' ? name : `${prefix}/${name}`;
3372
+ if (lstatSync(join(root, rel)).isDirectory()) out.push(...filesUnder(root, rel));
3373
+ else out.push(rel);
3374
+ }
3375
+ return out;
3376
+ }
3377
+
3378
+ /** The first way `copy` differs from `original`, or null when every file is the same bytes. */
3379
+ function firstDifference(copy: string, original: string): string | null {
3380
+ const theirs = filesUnder(copy);
3381
+ const ours = filesUnder(original);
3382
+ for (const rel of ours) {
3383
+ if (!theirs.includes(rel)) return `${rel} is missing from it`;
3384
+ if (!readFileSync(join(copy, rel)).equals(readFileSync(join(original, rel)))) return `${rel} differs`;
3385
+ }
3386
+ for (const rel of theirs) if (!ours.includes(rel)) return `${rel} is in it and not in the package`;
3387
+ return null;
3388
+ }
3389
+
3390
+ /**
3391
+ * Install every shipped skill into `dir`, or refuse and write nothing.
3392
+ *
3393
+ * The plan is made in full before the first write: every entry is classified as
3394
+ * absent, already this command's, or in the way, and one entry in the way
3395
+ * refuses the whole call with every such entry named.
3396
+ */
3397
+ function installSkills(source: string, dir: string, copy: boolean): SkillsInstallEntry[] {
3398
+ const names = shippedSkills(source);
3399
+ if (names.length === 0) {
3400
+ throw new SkillsInstallError(
3401
+ `no skill to install: ${source} ${existsSync(source) ? 'holds no <name>/SKILL.md' : 'is not there'}, and it is ` +
3402
+ 'the skills/ directory of the package this command ran from; nothing was written',
3403
+ );
3404
+ }
3405
+ if (existsSync(dir) && !statSync(dir).isDirectory()) {
3406
+ throw new SkillsInstallError(`${dir} exists and is not a directory, so no skill can be installed into it; nothing was written`);
3407
+ }
3408
+ const realDir = realpathAhead(dir);
3409
+ const planned: SkillsInstallEntry[] = [];
3410
+ const refused: string[] = [];
3411
+ for (const name of names) {
3412
+ const target = join(dir, name);
3413
+ const from = join(source, name);
3414
+ const realFrom = realpathSync(from);
3415
+ const link = relative(realDir, realFrom);
3416
+ let action: SkillsInstallAction = copy ? 'copied' : 'linked';
3417
+ let found: string | null = null;
3418
+ const stat = existsSync(target) || isLink(target) ? lstatSync(target) : null;
3419
+ if (stat === null) {
3420
+ // absent: this command writes it
3421
+ } else if (stat.isSymbolicLink()) {
3422
+ const text = readlinkSync(target);
3423
+ const pointsAt = resolve(realDir, text);
3424
+ const lands = existsSync(pointsAt) ? realpathSync(pointsAt) : null;
3425
+ if (lands === realFrom && !copy) action = 'already linked';
3426
+ else if (lands === realFrom) found = `a symlink to ${text}, the package's own folder, and --copy asks for a directory in its place`;
3427
+ else found = `a symlink to ${text}, ${lands === null ? 'which resolves to nothing' : `which resolves to ${lands}`}`;
3428
+ } else if (stat.isDirectory()) {
3429
+ const difference = copy ? firstDifference(target, from) : null;
3430
+ if (!copy) found = 'a directory';
3431
+ else if (difference === null) action = 'already copied';
3432
+ else found = `a directory that is not the package's copy (${difference})`;
3433
+ } else {
3434
+ found = 'a plain file';
3435
+ }
3436
+ if (found !== null) refused.push(`${target} is ${found}; ${copy ? `a copy of ${from}` : `a symlink to ${link}`} was required`);
3437
+ else planned.push({ target, source: from, action, link });
3438
+ }
3439
+ if (refused.length > 0) {
3440
+ throw new SkillsInstallError(
3441
+ `${refused.length} of the ${names.length} skill(s) cannot be installed into ${dir}, and nothing was written:\n` +
3442
+ refused.map((line) => ` ${line}`).join('\n') +
3443
+ '\nRemove the entries named above, or pass --dir to install somewhere else.',
3444
+ );
3445
+ }
3446
+ mkdirSync(dir, { recursive: true });
3447
+ for (const entry of planned) {
3448
+ if (entry.action === 'linked') symlinkSync(entry.link, entry.target, 'dir');
3449
+ else if (entry.action === 'copied') cpSync(entry.source, entry.target, { recursive: true, errorOnExist: true, force: false });
3450
+ }
3451
+ return planned;
3452
+ }
3453
+
3454
+ /** A dangling link is not `existsSync`, and is still an entry in the way. */
3455
+ function isLink(path: string): boolean {
3456
+ try {
3457
+ return lstatSync(path).isSymbolicLink();
3458
+ } catch {
3459
+ return false;
3460
+ }
3461
+ }
3462
+
3463
+ function cmdSkills(flags: Record<string, string>, positional: string[]): void {
3464
+ const [sub, ...extra] = positional;
3465
+ if (sub === undefined) {
3466
+ throw new UsageError(
3467
+ `skills takes a subcommand: ${SKILLS_SUBCOMMANDS.join(', ')} — \`rigc skills install\` links every skill this ` +
3468
+ `package ships into ${DEFAULT_SKILLS_DIR}`,
3469
+ );
3470
+ }
3471
+ if (!SKILLS_SUBCOMMANDS.includes(sub)) {
3472
+ throw new UsageError(`unknown skills subcommand: ${sub} (rigc skills offers ${SKILLS_SUBCOMMANDS.join(', ')})`);
3473
+ }
3474
+ if (extra.length > 0) {
3475
+ throw new UsageError(
3476
+ `skills install takes no positional argument, and ${JSON.stringify(extra[0])} was given — the directory is --dir <path>`,
3477
+ );
3478
+ }
3479
+ const takes = COMMANDS.find((c) => c.name === 'skills')?.flags ?? [];
3480
+ const foreign = Object.keys(flags).filter((flag) => !takes.includes(flag));
3481
+ if (foreign.length > 0) {
3482
+ throw new UsageError(
3483
+ `skills install takes ${takes.map((flag) => `--${flag}`).join(' and ')}; ` +
3484
+ `${foreign.map((flag) => `--${flag}`).join(', ')} is not one of them`,
3485
+ );
3486
+ }
3487
+ const copy = flags.copy !== undefined;
3488
+ const dir = resolve(process.cwd(), flags.dir ?? DEFAULT_SKILLS_DIR);
3489
+ const entries = installSkills(join(import.meta.dir, 'skills'), dir, copy);
3490
+ for (const entry of entries) {
3491
+ const ends = entry.action.endsWith('linked') ? `${entry.target} -> ${entry.link} (${entry.source})` : `${entry.target} <- ${entry.source}`;
3492
+ console.log(` ${entry.action.padEnd(14)} ${ends}`);
3493
+ }
3494
+ const wrote = entries.filter((entry) => entry.action === 'linked' || entry.action === 'copied').length;
3495
+ const verb = copy ? 'copied' : 'linked';
3496
+ console.log(
3497
+ wrote === 0
3498
+ ? `rigc skills install: nothing to do — all ${entries.length} skill(s) are already ${verb} into ${dir}`
3499
+ : `rigc skills install: ${wrote} of ${entries.length} skill(s) ${verb} into ${dir}` +
3500
+ (wrote < entries.length ? `, ${entries.length - wrote} already there` : ''),
3501
+ );
3502
+ }
3503
+
3273
3504
  // ---------------------------------------------------------------------------
3274
3505
  // usage / per-command help
3275
3506
  // ---------------------------------------------------------------------------
@@ -3368,6 +3599,14 @@ const FLAG_MEANINGS: Record<string, string> = {
3368
3599
  ballot: `the ballot the --record'd vote answers (default \`${DEFAULT_BALLOT}\`); its embedded manifest is what the vote is checked against`,
3369
3600
  ledger: `the append-only JSONL the vote lands in (default \`${DEFAULT_LEDGER}\`)`,
3370
3601
  again: 'record a second vote on a ballot the ledger already has; without it, a repeat is refused rather than doubled',
3602
+ dir:
3603
+ `the directory to install into, resolved against your working directory (default \`${DEFAULT_SKILLS_DIR}\`, the ` +
3604
+ 'workspace directory Codex, Gemini CLI and Antigravity read skills from)',
3605
+ copy:
3606
+ 'copy each skill folder instead of linking it, for a host that does not follow a linked skill folder. A copy ' +
3607
+ 'is not reached by an upgrade of the package, and one that is no longer the package\'s bytes is refused by name ' +
3608
+ 'on the next run — remove it and run again (default: a relative symlink, which an upgrade reaches with no ' +
3609
+ 'second run)',
3371
3610
  name: "the rig spec's own name, which the motion spec's archetype must match (default: the skeleton file's basename)",
3372
3611
  art: 'how the written spec reaches the art, which a skeleton does not encode: `loose` names an image per ' +
3373
3612
  "attachment, measured out of the rig spec's own images directory (--images writes it; without it, `build " +
@@ -3430,6 +3669,7 @@ const FLAG_VALUES: Record<string, string> = {
3430
3669
  name: '<n>',
3431
3670
  art: 'loose|none',
3432
3671
  stage: '<x,y,w,h>',
3672
+ dir: '<path>',
3433
3673
  };
3434
3674
 
3435
3675
  interface CommandDoc {
@@ -3723,6 +3963,21 @@ const COMMANDS: CommandDoc[] = [
3723
3963
  },
3724
3964
  },
3725
3965
  },
3966
+ {
3967
+ name: 'skills',
3968
+ usage: [`rigc skills install [--dir ${DEFAULT_SKILLS_DIR}] [--copy] (every skill this package ships, where an agent host looks)`],
3969
+ flags: ['dir', 'copy'],
3970
+ notes: [
3971
+ 'the skills installed are the skills/ directory of the package this command runs from,',
3972
+ 'never whatever the working directory holds. Each becomes <dir>/<name>: a relative',
3973
+ 'symlink into that folder, or with --copy a copy of it. An entry already there that',
3974
+ 'is not a link to the same folder — or, with --copy, not the same bytes — is refused',
3975
+ 'by name, exit 1, and nothing is written; a run over only what this command made has',
3976
+ 'nothing to do and exits 0. Codex, Gemini CLI and Antigravity read',
3977
+ `<workspace>/${DEFAULT_SKILLS_DIR}; Claude Code installs the plugin instead (README,`,
3978
+ '"Install it into your agent").',
3979
+ ],
3980
+ },
3726
3981
  ];
3727
3982
 
3728
3983
  const KNOWN_COMMANDS = COMMANDS.map((c) => c.name);
@@ -3829,6 +4084,13 @@ const USAGE = [
3829
4084
  'answer rather than a missing one, and a result whose hashes are not the ballot\'s is',
3830
4085
  'refused by name instead of appended.',
3831
4086
  '',
4087
+ 'skills install puts the agent skills this package ships where an agent host looks',
4088
+ 'for them, since none of them reads node_modules:',
4089
+ ` rigc skills install relative links in ${DEFAULT_SKILLS_DIR}, which Codex, Gemini CLI`,
4090
+ ' and Antigravity read; --copy writes the folders instead',
4091
+ 'An entry already there that this command did not make is refused by name and',
4092
+ 'nothing is written; a second run has nothing to do. See `rigc skills --help`.',
4093
+ '',
3832
4094
  'a cuts.json is { "<name>": { "rig": "...", "motion": "...", "out": "...",',
3833
4095
  ' "manifest": "..." (optional) } }, with every path',
3834
4096
  'resolved relative to the cuts.json file itself.',
@@ -3870,6 +4132,7 @@ try {
3870
4132
  else if (command === 'pose') cmdPose(flags);
3871
4133
  else if (command === 'chainfit') cmdChainFit(flags);
3872
4134
  else if (command === 'vote') cmdVote(flags, lists.candidate ?? []);
4135
+ else if (command === 'skills') cmdSkills(flags, positional);
3873
4136
  } catch (err) {
3874
4137
  if (err instanceof UsageError) {
3875
4138
  console.error(`rigc: ${err.message}\n\n${USAGE}`);
@@ -3940,5 +4203,13 @@ try {
3940
4203
  console.error(`rigc: ${err.message}`);
3941
4204
  process.exit(1);
3942
4205
  }
4206
+ // An install refused before its first write (issue #831): the invocation was
4207
+ // fine and an entry on disk was not what this command would write, so exit 1
4208
+ // like a file that is not a PNG. The message names every such entry, what is
4209
+ // there and what was required; the usage under it would bury that.
4210
+ if (err instanceof SkillsInstallError) {
4211
+ console.error(`rigc skills install: ${err.message}`);
4212
+ process.exit(1);
4213
+ }
3943
4214
  throw err;
3944
4215
  }
package/docs/AUTHORING.md CHANGED
@@ -186,6 +186,8 @@ What the flags mean:
186
186
  | `--min-visible` | `chainfit` only: below this share of a part surviving the parts drawn over it, the placement is refused `occluded` instead of reported flat (default `0.25`) — §12.4 |
187
187
  | `--passes` | `chainfit` only: how many times the occluder masks are rebuilt from the answers and the fit rerun (default `2`) — §12.4 |
188
188
  | `--anchor-residual` | `chainfit` only: the residual a `pose` placement must be within to anchor a chain (default `0.16`) — §12.2 |
189
+ | `--dir` | `skills install` only: the directory to install the agent skills into, resolved against your working directory (default `.agents/skills`, which Codex, Gemini CLI and Antigravity read). The skills are the `skills/` directory of the package the command runs from, never the working directory's |
190
+ | `--copy` | `skills install` only: copy each skill folder instead of writing a relative symlink to it. A copy is not reached by an upgrade of the package; one that is no longer the package's bytes is refused on the next run, naming the first file that differs |
189
191
 
190
192
  `render` also takes `--fps <n>` (the rate it samples at, default 12 — the same
191
193
  protocol rate the reference frames use) and `--max <px>` (the long side of a
@@ -193,6 +195,26 @@ frame, default 256). Three commands take `--out`: a directory for `render`
193
195
  (default `render/`), the `.html` file for `preview` (default `preview.html`) and
194
196
  for `vote` (default `ballot.html`).
195
197
 
198
+ `skills install` is the one command that is not about a rig: it puts the agent
199
+ skills the package ships where an agent host looks for them. Run it through the
200
+ project's own install — `bun rigc skills install` — so the links point into that
201
+ project's `node_modules/spine-rigc/skills/`. A second run has nothing to do and
202
+ exits 0. An entry already at `<dir>/<name>` that is not a link to the same folder
203
+ (or, under `--copy`, not the same bytes) is refused, exit 1, and **nothing is
204
+ written** — every such entry is named with what is there and what was required:
205
+
206
+ ```text
207
+ rigc skills install: <n> of the <m> skill(s) cannot be installed into <dir>, and nothing was written:
208
+ <dir>/rigc-motion is a plain file; a symlink to ../../node_modules/spine-rigc/skills/rigc-motion was required
209
+ Remove the entries named above, or pass --dir to install somewhere else.
210
+ ```
211
+
212
+ What is found is one of `a plain file`, `a directory`, `a symlink to <text>, which
213
+ resolves to <path>` (or `to nothing`), and under `--copy` `a directory that is not
214
+ the package's copy (<file> differs)`. A link to the package's own folder is not in
215
+ the way however it is spelled, so an absolute link somebody else made is kept and
216
+ reported `already linked`.
217
+
196
218
  Pick the profile deliberately, and know which one you got by saying nothing. The
197
219
  default is `spine`: "is this valid Spine 4.3 that any runtime plays correctly",
198
220
  which is the question a rig authored anywhere is asking. `--profile spine-html`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,8 +34,13 @@ whole interface, and this skill only says which of them to open.
34
34
  ```shell
35
35
  bunx spine-rigc --help # run it without installing
36
36
  bun add -d spine-rigc # or pin it in the project; the command is `rigc`
37
+ bun rigc skills install # then link these skills into .agents/skills
37
38
  ```
38
39
 
40
+ Codex, Gemini CLI and Antigravity read skills from `.agents/skills/` in the
41
+ workspace and none of them reads `node_modules`; `rigc skills install` puts every
42
+ skill the package ships there, and `rigc skills --help` says what it refuses.
43
+
39
44
  ## The loop
40
45
 
41
46
  1. `rigc build --rig <spec> --motion <spec> --images <dir> --out <dir>` compiles,
@@ -61,17 +66,19 @@ AUTHORING §0.
61
66
 
62
67
  ## Which guide, for which need
63
68
 
64
- Read [AUTHORING.md](../../docs/AUTHORING.md) first, whatever the need: the two
69
+ Read [AUTHORING.md](https://github.com/firejune/rigc/blob/main/docs/AUTHORING.md) first, whatever the need: the two
65
70
  spec files field by field, the emission rules, the loop, and the failure map. Then:
66
71
 
67
72
  | The request is… | Open | Skill |
68
73
  | --- | --- | --- |
69
- | a **skeleton** — how many bones, where each pivot sits, what hangs off what | [RIGGING.md](../../docs/RIGGING.md) | `rigging` |
70
- | a **movement** — an idle, a loop, from this pose to that one | [MOTION.md](../../docs/MOTION.md) | `motion` |
71
- | a **face** — a blink, a gaze, a head turn a few degrees off axis | [FACE.md](../../docs/FACE.md) | `face` |
72
- | a **skeleton.json somebody else authored** — read it, repair it, extend it | [INGEST.md](../../docs/INGEST.md) | `ingest` |
73
- | you are the **person operating** the agent rather than the agent | [PROMPTING.md](../../docs/PROMPTING.md) | — |
74
+ | a **skeleton** — how many bones, where each pivot sits, what hangs off what | [RIGGING.md](https://github.com/firejune/rigc/blob/main/docs/RIGGING.md) | `rigc-rigging` |
75
+ | a **movement** — an idle, a loop, from this pose to that one | [MOTION.md](https://github.com/firejune/rigc/blob/main/docs/MOTION.md) | `rigc-motion` |
76
+ | a **face** — a blink, a gaze, a head turn a few degrees off axis | [FACE.md](https://github.com/firejune/rigc/blob/main/docs/FACE.md) | `rigc-face` |
77
+ | a **skeleton.json somebody else authored** — read it, repair it, extend it | [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) | `rigc-ingest` |
78
+ | you are the **person operating** the agent rather than the agent | [PROMPTING.md](https://github.com/firejune/rigc/blob/main/docs/PROMPTING.md) | — |
74
79
 
75
- The same files are in the installed package at `node_modules/spine-rigc/docs/`;
76
- inside this plugin they are at `${CLAUDE_PLUGIN_ROOT}/docs/`. Formats, the CLI
77
- reference and the licence chain: [README.md](../../README.md).
80
+ Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
81
+ which is the copy that matches the rigc you run; the links go to the repository's
82
+ `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
83
+ Formats, the CLI reference and the licence chain:
84
+ [README.md](https://github.com/firejune/rigc/blob/main/README.md), installed at `node_modules/spine-rigc/README.md`.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: face
2
+ name: rigc-face
3
3
  description: Author a face on plain Spine data with rigc — a blink, a gaze shift, a breathing portrait and a head turn a few degrees off axis, built from deform timelines and per-part parallax. Use when the request is a talking or living portrait, a standing character, an expression or a head turn, such as "rig this face", "make the portrait blink and look around" or "turn the head". Not for Live2D file conversion, cutting a face illustration into parts, or VTuber-style real-time face tracking.
4
4
  license: MIT
5
5
  compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
@@ -9,7 +9,7 @@ compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
9
9
 
10
10
  Load this when the request is a **head rather than a body**: a drawn face that
11
11
  breathes, blinks, moves its eyes and turns a few degrees off axis. Every rule below
12
- is owned by [FACE.md](../../docs/FACE.md); this file says when to open it and what
12
+ is owned by [FACE.md](https://github.com/firejune/rigc/blob/main/docs/FACE.md); this file says when to open it and what
13
13
  it will not do for you.
14
14
 
15
15
  ## Non-negotiables
@@ -40,17 +40,21 @@ differential audit and the three limits it does not lift.
40
40
 
41
41
  ## Read, in this order
42
42
 
43
- 1. [AUTHORING.md](../../docs/AUTHORING.md) — the `deform` timeline field by field
43
+ 1. [AUTHORING.md](https://github.com/firejune/rigc/blob/main/docs/AUTHORING.md) — the `deform` timeline field by field
44
44
  and what rigc refuses in it (§4), the failure map (§5–§6), the editor's
45
45
  conventions (§10).
46
- 2. [MOTION.md](../../docs/MOTION.md) — timing, easing and the offset table (§3),
46
+ 2. [MOTION.md](https://github.com/firejune/rigc/blob/main/docs/MOTION.md) — timing, easing and the offset table (§3),
47
47
  candidates and the ballot (§4–§5). A blink and a gaze are ordinary motion work.
48
- 3. [FACE.md](../../docs/FACE.md) — the face's own geometry: the closed form every
48
+ 3. [FACE.md](https://github.com/firejune/rigc/blob/main/docs/FACE.md) — the face's own geometry: the closed form every
49
49
  number in a turn comes from (§1), the hierarchy underneath (§3), what
50
50
  foreshortens (§5), and the deform audit gap (§9).
51
- 4. Then [RIGGING.md](../../docs/RIGGING.md) for the hierarchy as a general rule
52
- rather than this closed form, and [INGEST.md](../../docs/INGEST.md) if the head
51
+ 4. Then [RIGGING.md](https://github.com/firejune/rigc/blob/main/docs/RIGGING.md) for the hierarchy as a general rule
52
+ rather than this closed form, and [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) if the head
53
53
  arrived as a compiled skeleton.
54
54
 
55
+ Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
56
+ which is the copy that matches the rigc you run; the links go to the repository's
57
+ `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
58
+
55
59
  The install line and the build → validate → render → check loop are in the `rigc`
56
60
  skill.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: ingest
2
+ name: rigc-ingest
3
3
  description: Work with a Spine skeleton.json somebody else authored — exported from the Spine editor or another tool — using rigc. Read and validate it, understand a complaint rigc raised about it, decompile it into rigc specs with `rigc ingest`, normalise, re-pivot or rename it, and extend it with an animation it does not have. Use when the input is an existing skeleton.json with its .atlas and page images rather than loose part PNGs. Not for Live2D file conversion or runtime tracking.
4
4
  license: MIT
5
5
  compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
@@ -10,7 +10,7 @@ compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
10
10
  Load this when what you were handed is **already a skeleton**: a `skeleton.json`
11
11
  with its `.atlas` and page images, and a request to understand it, answer a
12
12
  complaint about it, re-express it, or extend it. Every rule below is owned by
13
- [INGEST.md](../../docs/INGEST.md); this file says when to open it and what it will
13
+ [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md); this file says when to open it and what it will
14
14
  not do for you.
15
15
 
16
16
  ## Non-negotiables
@@ -61,14 +61,18 @@ thing to reach for; transcription by hand is what you fall back on for a constru
61
61
 
62
62
  ## Read, in this order
63
63
 
64
- 1. [INGEST.md](../../docs/INGEST.md) — what every command will and will not do with
64
+ 1. [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) — what every command will and will not do with
65
65
  a foreign file (§0), `ingest` and transcription (§2), what each validator
66
66
  complaint means on an export (§3), and the re-pivot, rename and extend
67
67
  recipes (§4).
68
- 2. [AUTHORING.md](../../docs/AUTHORING.md) — the two spec files the transcription
68
+ 2. [AUTHORING.md](https://github.com/firejune/rigc/blob/main/docs/AUTHORING.md) — the two spec files the transcription
69
69
  targets (§3–§4), the failure map (§5–§6), and the coordinate contract (§11.2).
70
- 3. Then [RIGGING.md](../../docs/RIGGING.md) for why the re-pivot edit has the shape
71
- it has, and [MOTION.md](../../docs/MOTION.md) for the animation you are adding.
70
+ 3. Then [RIGGING.md](https://github.com/firejune/rigc/blob/main/docs/RIGGING.md) for why the re-pivot edit has the shape
71
+ it has, and [MOTION.md](https://github.com/firejune/rigc/blob/main/docs/MOTION.md) for the animation you are adding.
72
+
73
+ Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
74
+ which is the copy that matches the rigc you run; the links go to the repository's
75
+ `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
72
76
 
73
77
  The install line and the build → validate → render → check loop are in the `rigc`
74
78
  skill.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: motion
2
+ name: rigc-motion
3
3
  description: Author a Spine animation with rigc from key poses — an idle, a loop, a move from one picture to another — with timing and spacing, ease in and out, anticipation, arcs, overlap, follow-through, squash and stretch, and candidate variants a person can choose between. Use when the request is a movement on an existing or planned rig, in the animator's words too: "animate this rig", "make it breathe", "go from pose A to pose B", "make it feel heavier", "the cape should follow through". Not for Live2D, separating an image into parts, or real-time tracking.
4
4
  license: MIT
5
5
  compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
@@ -10,7 +10,7 @@ compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
10
10
  Load this when the request is a **movement rather than a skeleton**: a sentence of
11
11
  intent, between zero and N pictures of what the movement passes through, and a
12
12
  Spine animation somebody would choose coming back. Every rule below is owned by
13
- [MOTION.md](../../docs/MOTION.md); this file says when to open it and what it will
13
+ [MOTION.md](https://github.com/firejune/rigc/blob/main/docs/MOTION.md); this file says when to open it and what it will
14
14
  not do for you.
15
15
 
16
16
  ## Non-negotiables
@@ -31,17 +31,21 @@ thing that judges a movement is a person's eye through `rigc vote` — MOTION §
31
31
 
32
32
  ## Read, in this order
33
33
 
34
- 1. [AUTHORING.md](../../docs/AUTHORING.md) — the motion spec field by field (§4),
34
+ 1. [AUTHORING.md](https://github.com/firejune/rigc/blob/main/docs/AUTHORING.md) — the motion spec field by field (§4),
35
35
  the failure map (§5–§6), reading reference frames (§8) and checking against
36
36
  them (§9), what the editor does when nobody tells it otherwise (§10), and
37
37
  reading a pose out of a picture (§11–§12).
38
- 2. [MOTION.md](../../docs/MOTION.md) — the normal form every motion request
38
+ 2. [MOTION.md](https://github.com/firejune/rigc/blob/main/docs/MOTION.md) — the normal form every motion request
39
39
  reduces to (§0), timing, easing and the per-bone offset table (§3), and how to
40
40
  spread candidates so a ballot informs (§4–§5).
41
- 3. Then [RIGGING.md](../../docs/RIGGING.md) if the skeleton itself is what you have
42
- to decide; [FACE.md](../../docs/FACE.md) if the movement is a blink, a gaze or a
43
- head turn; [INGEST.md](../../docs/INGEST.md) if the rig arrived as a compiled
41
+ 3. Then [RIGGING.md](https://github.com/firejune/rigc/blob/main/docs/RIGGING.md) if the skeleton itself is what you have
42
+ to decide; [FACE.md](https://github.com/firejune/rigc/blob/main/docs/FACE.md) if the movement is a blink, a gaze or a
43
+ head turn; [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) if the rig arrived as a compiled
44
44
  skeleton rather than loose parts.
45
45
 
46
+ Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
47
+ which is the copy that matches the rigc you run; the links go to the repository's
48
+ `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
49
+
46
50
  The install line and the build → validate → render → check loop are in the `rigc`
47
51
  skill.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rigging
2
+ name: rigc-rigging
3
3
  description: Decide a Spine rig's hierarchy with rigc — how many bones, where each pivot sits, what hangs off what, offsets, chains and what a chain can reach, siblings versus chains, constraints as structure — and which of those decisions the reference frames can check. Use when the request is a skeleton from loose part PNGs, such as "rig these parts", "make a Spine skeleton" or "where do the joints go", before any motion is authored. Not for Live2D, cutting an illustration into parts, or VTuber-style tracking.
4
4
  license: MIT
5
5
  compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
@@ -9,7 +9,7 @@ compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
9
9
 
10
10
  Load this when the request is a **skeleton rather than a movement**: loose part
11
11
  PNGs in, a bone hierarchy out. Every rule below is owned by
12
- [RIGGING.md](../../docs/RIGGING.md); this file says when to open it and what it
12
+ [RIGGING.md](https://github.com/firejune/rigc/blob/main/docs/RIGGING.md); this file says when to open it and what it
13
13
  will not do for you.
14
14
 
15
15
  ## Non-negotiables
@@ -30,16 +30,20 @@ are named in RIGGING §11 — neither as a pass bar.
30
30
 
31
31
  ## Read, in this order
32
32
 
33
- 1. [AUTHORING.md](../../docs/AUTHORING.md) — the rig spec field by field (§3),
33
+ 1. [AUTHORING.md](https://github.com/firejune/rigc/blob/main/docs/AUTHORING.md) — the rig spec field by field (§3),
34
34
  the failure map (§5–§6), reading a pose out of a picture (§8.1, §11, §12),
35
35
  and the editor's own conventions (§10).
36
- 2. [RIGGING.md](../../docs/RIGGING.md) — the structure itself: what identifies a
36
+ 2. [RIGGING.md](https://github.com/firejune/rigc/blob/main/docs/RIGGING.md) — the structure itself: what identifies a
37
37
  pivot and what moving one costs (§2–§3), gauges (§4), what a chain can reach
38
38
  (§6), and the instruments that see structure (§11).
39
- 3. Then [MOTION.md](../../docs/MOTION.md) once the skeleton exists;
40
- [FACE.md](../../docs/FACE.md) if the figure is a head;
41
- [INGEST.md](../../docs/INGEST.md) if the skeleton was handed to you already
39
+ 3. Then [MOTION.md](https://github.com/firejune/rigc/blob/main/docs/MOTION.md) once the skeleton exists;
40
+ [FACE.md](https://github.com/firejune/rigc/blob/main/docs/FACE.md) if the figure is a head;
41
+ [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) if the skeleton was handed to you already
42
42
  compiled.
43
43
 
44
+ Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
45
+ which is the copy that matches the rigc you run; the links go to the repository's
46
+ `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
47
+
44
48
  The install line and the build → validate → render → check loop are in the `rigc`
45
49
  skill.