skill-land 1.3.1 → 1.5.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
@@ -61,6 +61,7 @@ npx skill-land <source> --for <agent> [options]
61
61
  | `--project` | install into `./` instead of the user-global directory |
62
62
  | `--dry-run` | resolve and report, write nothing |
63
63
  | `--verify` | check an existing install, write nothing |
64
+ | `--discover` | also ask a fresh agent session to list its skills (claude-code, codex, gemini-cli, antigravity) |
64
65
  | `--skip-security` | skip the SkillSpector scan (acknowledged, not silent) |
65
66
  | `--strict` | refuse to install anything SkillSpector flags |
66
67
  | `--list` | show known agents and their directories |
@@ -133,6 +134,31 @@ Where an agent has several directories it reads, `--verify` counts the skill as
133
134
  it is **complete in any one of them**. Requiring every fallback to hold a copy reports
134
135
  working installs as broken.
135
136
 
137
+ ### Does the agent actually load it? `--discover`
138
+
139
+ Everything above proves the bytes are on disk. It does not prove the agent loads them: it may
140
+ need a restart, reject the frontmatter, or read the directory only in some workspaces.
141
+ `--discover` starts a fresh non-interactive session from a neutral directory and checks that the
142
+ skill name appears in what the agent says it can see.
143
+
144
+ ```bash
145
+ skill-land Magithar/SKILLmama --for codex,antigravity --discover
146
+ ```
147
+
148
+ | Agent | Listing used | Kind |
149
+ |---|---|---|
150
+ | Antigravity | `agy -p /skills` | a real skill list |
151
+ | Gemini CLI | `gemini skills list` | a real skill list, with enabled/disabled state |
152
+ | Codex | `codex exec "list your skills"` | asks the model |
153
+ | Claude Code | `claude -p "list your skills"` | asks the model |
154
+
155
+ Each was tested by planting a uniquely named canary skill and confirming it appeared, then with real skills from git. A skill that is listed but switched off (Gemini CLI prints `[Disabled]`) fails: on disk and listed is not the same as loaded. Every other
156
+ agent reports `NOT CHECKED`, never a pass: an unknown is not a success. A missing binary or a
157
+ timeout is also `NOT CHECKED`; only a completed listing that lacks the skill fails the run.
158
+
159
+ The Codex and Claude Code listings go through the model, so they cost an API call and a `FAIL`
160
+ is a lead to investigate, not proof. It is opt-in for that reason, and skipped with `--project`.
161
+
136
162
  ### What it cannot tell you
137
163
 
138
164
  Verification assumes the installer **copied**. Some installers **rewrite** on install — the
package/agents.json CHANGED
@@ -7,7 +7,15 @@
7
7
  "project": ".claude/skills",
8
8
  "file": "SKILL.md",
9
9
  "restart": false,
10
- "broken": false
10
+ "broken": false,
11
+ "discover": {
12
+ "cmd": [
13
+ "claude",
14
+ "-p",
15
+ "List the names of all skills available to you, one per line. Output only names."
16
+ ],
17
+ "deterministic": false
18
+ }
11
19
  },
12
20
  "codex": {
13
21
  "display": "OpenAI Codex",
@@ -15,7 +23,16 @@
15
23
  "project": ".agents/skills",
16
24
  "file": "SKILL.md",
17
25
  "restart": false,
18
- "broken": true
26
+ "broken": true,
27
+ "discover": {
28
+ "cmd": [
29
+ "codex",
30
+ "exec",
31
+ "--skip-git-repo-check",
32
+ "List the names of all skills available to you, one per line. Output only names."
33
+ ],
34
+ "deterministic": false
35
+ }
19
36
  },
20
37
  "antigravity": {
21
38
  "_paths": "LIVE VERIFIED against agy 1.1.27 on 2026-09-05: a uniquely-named canary skill was planted in each candidate directory and `agy -p /skills` run from a neutral workspace. Read as global: ~/.gemini/config/skills, ~/.gemini/skills, ~/.gemini/antigravity-cli/skills — matching the maintainer statement in google-antigravity/antigravity-cli#103 comment 5547952239. NOT read: ~/.agents/skills (appears only when its parent is the active workspace, i.e. `--add-dir $HOME`, which is project-local behavior, not global) and ~/.gemini/antigravity/skills (app.asar defines ~/.gemini/antigravity as IDE_OLD_DATA_DIR, the legacy IDE data dir migrating to ~/.gemini/antigravity-ide; never a skills root). Project dir .agents/skills is read only when the directory is a registered workspace — cwd alone was not enough; .antigravity/skills and .agy/skills are not read at all. language_server hardcodes no .gemini/*/skills literal: skills_paths is a repeated protobuf field the client fills in.",
@@ -28,7 +45,15 @@
28
45
  "project": ".agents/skills",
29
46
  "file": "SKILL.md",
30
47
  "restart": true,
31
- "broken": true
48
+ "broken": true,
49
+ "discover": {
50
+ "cmd": [
51
+ "agy",
52
+ "-p",
53
+ "/skills"
54
+ ],
55
+ "deterministic": true
56
+ }
32
57
  },
33
58
  "antigravity-cli": {
34
59
  "display": "Antigravity CLI",
@@ -36,7 +61,15 @@
36
61
  "project": ".agents/skills",
37
62
  "file": "SKILL.md",
38
63
  "restart": true,
39
- "broken": true
64
+ "broken": true,
65
+ "discover": {
66
+ "cmd": [
67
+ "agy",
68
+ "-p",
69
+ "/skills"
70
+ ],
71
+ "deterministic": true
72
+ }
40
73
  },
41
74
  "gemini-cli": {
42
75
  "display": "Gemini CLI",
@@ -44,7 +77,15 @@
44
77
  "project": ".agents/skills",
45
78
  "file": "SKILL.md",
46
79
  "restart": false,
47
- "broken": true
80
+ "broken": true,
81
+ "discover": {
82
+ "cmd": [
83
+ "gemini",
84
+ "skills",
85
+ "list"
86
+ ],
87
+ "deterministic": true
88
+ }
48
89
  },
49
90
  "cursor": {
50
91
  "display": "Cursor",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-land",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Install an agent skill to the directory the agent actually reads, then verify it landed. Exits non-zero if it didn't.",
5
5
  "keywords": [
6
6
  "agent-skills",
package/skill-land.mjs CHANGED
@@ -514,7 +514,57 @@ function runSkill(skillPath, root, quiet) {
514
514
  console.log(` x ${b.destDir} ${b.absent ? "not installed" : `${b.n}/${files.length} files`}`);
515
515
  }
516
516
  }
517
- return { name: skillName, failed: skillFailed, skipped: skillSkipped };
517
+ return { name: skillName, fmName: fm.name, failed: skillFailed, skipped: skillSkipped };
518
+ }
519
+
520
+ // ---------------------------------------------------------------- discover
521
+
522
+ // --verify proves the bytes are on disk. It cannot prove the agent loads them:
523
+ // the agent may need a restart, reject the frontmatter, or read the directory
524
+ // only in some workspaces. --discover asks a fresh, non-interactive session what
525
+ // skills it sees. Run from a neutral cwd so a project-local skills directory
526
+ // cannot make a missing global install look found.
527
+ //
528
+ // Only agents with a `discover` entry in agents.json were checked this way, and
529
+ // each was tested with a planted canary. The rest report "not checked" rather
530
+ // than pass: an unknown is not a success.
531
+ const hasName = (out, name) =>
532
+ new RegExp(`(^|[^\\w-])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}($|[^\\w-])`, "m").test(out);
533
+
534
+ function discover(agent, names) {
535
+ const a = AGENTS[agent];
536
+ console.log(` ${a.display}`);
537
+ if (!a.discover) {
538
+ console.log(` NOT CHECKED no known non-interactive skill listing for ${a.display}\n`);
539
+ return 0;
540
+ }
541
+ const [bin, ...args] = a.discover.cmd;
542
+ const cwd = mkdtempSync(join(tmpdir(), "skill-land-discover-"));
543
+ let out;
544
+ try {
545
+ out = execFileSync(bin, args, { cwd, encoding: "utf-8", timeout: 180000,
546
+ stdio: ["ignore", "pipe", "pipe"] });
547
+ } catch (e) {
548
+ const why = e.code === "ENOENT" ? `${bin} not on PATH` : (e.message.split("\n")[0]);
549
+ console.log(` NOT CHECKED ${why}\n`);
550
+ return 0;
551
+ } finally {
552
+ rmSync(cwd, { recursive: true, force: true });
553
+ }
554
+ let missing = 0;
555
+ for (const n of names) {
556
+ const found = hasName(out, n);
557
+ // Listed but switched off is not loaded. Gemini CLI prints `name [Disabled]`.
558
+ const disabled = found && new RegExp(`(^|\\s)${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} \\[Disabled\\]`, "m").test(out);
559
+ const ok = found && !disabled;
560
+ if (!ok) missing++;
561
+ console.log(` ${ok ? "OK " : "FAIL"} ${n} ${disabled ? "listed but disabled" : found ? "listed" : "not in the skill list of a fresh session"}`);
562
+ }
563
+ if (!a.discover.deterministic)
564
+ console.log(` note ${a.display} has no plain skill listing; this asks the model, so treat a FAIL as a lead, not proof`);
565
+ if (missing && a.restart) console.log(` note restart ${a.display} fully; skills load at startup`);
566
+ console.log();
567
+ return missing;
518
568
  }
519
569
 
520
570
  // ---------------------------------------------------------------- main
@@ -534,6 +584,8 @@ if (!argv.length || argv.includes("-h") || argv.includes("--help")) {
534
584
  --project install into ./ instead of the user-global dir
535
585
  --dry-run resolve and report, write nothing
536
586
  --verify check an existing install, write nothing
587
+ --discover also ask a fresh agent session to list its skills
588
+ (claude-code, codex, gemini-cli, antigravity; others: not checked)
537
589
  --skip-security skip the SkillSpector scan (acknowledged, not silent)
538
590
  --strict refuse to install anything SkillSpector flags
539
591
  --list show known agents and their directories
@@ -570,6 +622,7 @@ for (const t of targets) if (!AGENTS[t]) die(`unknown agent "${t}"\n kno
570
622
  const isProject = flag("--project");
571
623
  const dryRun = flag("--dry-run");
572
624
  const verifyOnly = flag("--verify");
625
+ const discoverFlag = flag("--discover");
573
626
  const skipSecurity = flag("--skip-security");
574
627
  const strict = flag("--strict");
575
628
  const allSkills = flag("--all");
@@ -584,6 +637,7 @@ const src = resolveSource(source);
584
637
  const tmp = mkdtempSync(join(tmpdir(), "skill-install-"));
585
638
  let failed = 0;
586
639
  const skipped = [];
640
+ const landed = [];
587
641
 
588
642
  try {
589
643
  const root = fetchSource(src, tmp);
@@ -598,6 +652,7 @@ try {
598
652
  const r = runSkill(skillPath, root, quiet);
599
653
  failed += r.failed;
600
654
  if (r.skipped) skipped.push(r.name);
655
+ else if (!r.failed) landed.push(r.fmName || r.name);
601
656
  }
602
657
 
603
658
  if (allSkills) {
@@ -610,6 +665,17 @@ try {
610
665
  rmSync(tmp, { recursive: true, force: true });
611
666
  }
612
667
 
668
+ // Once per agent, not per skill: a listing is one session however many skills.
669
+ // Only skills that passed file verification are looked up; a skill that never
670
+ // landed would just produce a second, redundant failure.
671
+ if (discoverFlag && !dryRun) {
672
+ if (isProject) console.log(" --discover is skipped with --project: the listing runs from a neutral directory\n");
673
+ else if (landed.length) {
674
+ console.log(" discovery fresh session, neutral directory\n");
675
+ for (const t of targets) failed += discover(t, landed);
676
+ }
677
+ }
678
+
613
679
  if (failed) {
614
680
  console.error(` ${failed} install(s) failed verification\n`);
615
681
  process.exit(1);