@malloy-publisher/skills 0.1.33 → 0.1.35
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 +26 -1
- package/dist/cli.d.ts +27 -0
- package/dist/cli.js +188 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.js +8 -9
- package/dist/install.d.ts +68 -0
- package/dist/install.js +182 -0
- package/dist/payload.d.ts +8 -0
- package/dist/payload.js +19 -0
- package/package.json +4 -1
- package/skills/eval-answer/SKILL.md +1 -1
- package/skills/eval-answer/reference/ledger-schema.md +3 -3
- package/skills/eval-answer/scripts/check_findable.py +121 -53
- package/skills/eval-answer/scripts/check_findable_test.py +153 -0
- package/skills/eval-answer/scripts/ledger.py +2 -1
- package/skills/eval-answer/scripts/mcp_payload.py +13 -0
- package/skills/eval-diagnose/SKILL.md +2 -2
- package/skills/eval-import/SKILL.md +1 -1
- package/skills/eval-improve/SKILL.md +1 -1
- package/skills/eval-judge/SKILL.md +1 -1
- package/skills/eval-loop/SKILL.md +6 -4
- package/skills/eval-loop/reference/measurement.md +10 -6
- package/skills/eval-loop/reference/running-a-run.md +6 -6
- package/skills/eval-loop/scripts/agent_harness_test.py +73 -17
- package/skills/eval-loop/scripts/flip_table.py +20 -3
- package/skills/eval-loop/scripts/flip_table_test.py +15 -0
- package/skills/eval-loop/scripts/run_baseline.py +106 -45
- package/skills/eval-loop/scripts/run_baseline_test.py +16 -1
- package/skills/eval-loop/scripts/serve.py +63 -40
- package/skills/eval-loop/scripts/serve_test.py +62 -4
- package/skills/eval-loop/templates/eval-run-package/README.md +2 -1
- package/skills/eval-report/SKILL.md +1 -1
- package/skills/malloy/SKILL.md +2 -3
- package/skills/malloy-analysis/SKILL.md +6 -2
- package/skills/malloy-analysis-pitfalls/SKILL.md +1 -1
- package/skills/malloy-analysis-report/SKILL.md +6 -4
- package/skills/malloy-analyze/SKILL.md +4 -4
- package/skills/malloy-charts/SKILL.md +10 -2
- package/skills/malloy-dashboards/SKILL.md +99 -125
- package/skills/malloy-data-app-design/SKILL.md +1 -1
- package/skills/malloy-debug/SKILL.md +1 -1
- package/skills/malloy-define/SKILL.md +1 -1
- package/skills/malloy-discover/SKILL.md +1 -1
- package/skills/malloy-document/SKILL.md +2 -2
- package/skills/malloy-getting-started/SKILL.md +3 -1
- package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
- package/skills/malloy-gotchas-queries/SKILL.md +1 -1
- package/skills/malloy-gotchas-rendering/SKILL.md +2 -2
- package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
- package/skills/malloy-html-data-app-runtime/SKILL.md +1 -1
- package/skills/malloy-html-data-apps/SKILL.md +1 -1
- package/skills/malloy-lookml-review/SKILL.md +1 -1
- package/skills/malloy-materialization/SKILL.md +1 -1
- package/skills/malloy-materialization-tuning/SKILL.md +1 -1
- package/skills/malloy-model/SKILL.md +5 -3
- package/skills/malloy-model/reference/analysis-to-model.md +1 -1
- package/skills/malloy-model-as-you-go/SKILL.md +1 -1
- package/skills/malloy-modeling/SKILL.md +1 -1
- package/skills/malloy-notebooks/SKILL.md +110 -24
- package/skills/malloy-patterns/SKILL.md +1 -1
- package/skills/malloy-phrase-detection/SKILL.md +1 -1
- package/skills/malloy-powerbi-review/SKILL.md +1 -1
- package/skills/malloy-publish/SKILL.md +3 -3
- package/skills/malloy-queries/SKILL.md +1 -1
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-review/reference/rubric-structure.md +1 -1
- package/skills/malloy-review/reference/rubric-style.md +10 -1
- package/skills/malloy-review/reference/severity-taxonomy.md +1 -0
- package/skills/malloy-scope/SKILL.md +1 -1
- package/skills/malloy-source-unreachable/SKILL.md +3 -3
- package/skills/malloy-notebook-chat/SKILL.md +0 -25
package/README.md
CHANGED
|
@@ -5,6 +5,31 @@ SPDX-License-Identifier: MIT
|
|
|
5
5
|
|
|
6
6
|
# @malloy-publisher/skills
|
|
7
7
|
|
|
8
|
+
## Start here
|
|
9
|
+
|
|
10
|
+
1. **Learn.** Read the getting-started skill. Nothing to install, no server:
|
|
11
|
+
|
|
12
|
+
<https://unpkg.com/@malloy-publisher/skills@latest/skills/malloy-getting-started/SKILL.md>
|
|
13
|
+
|
|
14
|
+
Every skill reads the same way. Put its name in place of `malloy-getting-started`.
|
|
15
|
+
|
|
16
|
+
2. **Install.** If your agent has a shell, copy every skill to where it loads them:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx -y @malloy-publisher/skills@latest install # into this project
|
|
20
|
+
npx -y @malloy-publisher/skills@latest install --global # into your home directory
|
|
21
|
+
npx -y @malloy-publisher/skills@latest list # every skill, with a description
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Keep the `@latest`. Without it, npx may reuse an old cached copy and install old skills.
|
|
25
|
+
|
|
26
|
+
`install` picks the agent from what it finds. A `CLAUDE.md` means `.claude/skills/`. An
|
|
27
|
+
`AGENTS.md` or `.cursor/` means `.agents/skills/`. With `--global` it looks for `~/.claude/`,
|
|
28
|
+
`~/.agents/`, or `~/.cursor/` instead. If it finds none, it stops and asks you to name one:
|
|
29
|
+
`install claude` or `install agents`.
|
|
30
|
+
|
|
31
|
+
## What this package is
|
|
32
|
+
|
|
8
33
|
The agent skills that ship with [Malloy Publisher](https://github.com/malloydata/publisher), as
|
|
9
34
|
files you can install. They are task-specific guides for writing Malloy, building and reviewing
|
|
10
35
|
models, exploring data, and authoring HTML data apps.
|
|
@@ -14,7 +39,7 @@ files. This package exists for the cases MCP does not cover: a host that reads s
|
|
|
14
39
|
(Claude Code and the Publisher plugin), a scaffolder that installs them into a new project, or any
|
|
15
40
|
tool that wants the files themselves rather than prompt text.
|
|
16
41
|
|
|
17
|
-
##
|
|
42
|
+
## Use the files from code
|
|
18
43
|
|
|
19
44
|
```bash
|
|
20
45
|
npm install @malloy-publisher/skills
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Where each host looks for skills, relative to a project or to the home dir. */
|
|
3
|
+
declare const TARGETS: Record<string, string>;
|
|
4
|
+
/**
|
|
5
|
+
* What says a host is set up, per scope. The two differ because the hosts keep
|
|
6
|
+
* their user-level config in a directory rather than a top-level file: Claude
|
|
7
|
+
* Code reads `~/.claude/CLAUDE.md`, never `~/CLAUDE.md`, so the project markers
|
|
8
|
+
* would miss almost everyone who has Claude set up.
|
|
9
|
+
*/
|
|
10
|
+
declare const MARKERS: Record<"project" | "global", Record<string, string[]>>;
|
|
11
|
+
/**
|
|
12
|
+
* Where a skill can be read with nothing installed. `@latest` rather than this
|
|
13
|
+
* package's own version, so a link copied from an old cached copy of this CLI
|
|
14
|
+
* still serves the current skill.
|
|
15
|
+
*/
|
|
16
|
+
declare function skillUrl(name: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Which hosts this directory is set up for.
|
|
19
|
+
*
|
|
20
|
+
* Detection only ever chooses between hosts the user already has; it never
|
|
21
|
+
* decides *whether* to write. When it finds nothing, install asks rather than
|
|
22
|
+
* picking one, because a skills directory in the wrong place is invisible: the
|
|
23
|
+
* agent simply never loads them and nothing says why.
|
|
24
|
+
*/
|
|
25
|
+
declare function detectTargets(root: string, scope: keyof typeof MARKERS): string[];
|
|
26
|
+
declare function main(argv: string[]): void;
|
|
27
|
+
export { TARGETS, detectTargets, main, skillUrl };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Credible Data Inc.
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
/**
|
|
5
|
+
* `malloy-skills` -- list the agent skills, and install them, with nothing
|
|
6
|
+
* running.
|
|
7
|
+
*
|
|
8
|
+
* This exists so an agent meeting Publisher for the first time can get the
|
|
9
|
+
* skills before it installs or starts anything. Every other route to them is
|
|
10
|
+
* gated: the MCP prompt channel needs the server up, and the server is a large
|
|
11
|
+
* install that clones its example packages over the network.
|
|
12
|
+
*
|
|
13
|
+
* Reading one skill needs no command at all: the package ships the files, so
|
|
14
|
+
* unpkg serves each SKILL.md at a stable URL (`skillUrl`). That works in a chat
|
|
15
|
+
* with no shell, which `npx` cannot. This CLI covers what a URL cannot: listing
|
|
16
|
+
* what ships, and copying the whole tree, `reference/` files included, onto
|
|
17
|
+
* disk through the hardened copy in install.ts.
|
|
18
|
+
*
|
|
19
|
+
* Which is why this lives in @malloy-publisher/skills and not in the server or a
|
|
20
|
+
* new package: this one already ships the skill files, has no runtime
|
|
21
|
+
* dependencies and no native code, so `npx` is fast and cannot fail on a build
|
|
22
|
+
* step or a platform binary. Adding a CLI framework here would cost that, for
|
|
23
|
+
* two commands, so the argument parsing below is by hand.
|
|
24
|
+
*/
|
|
25
|
+
import * as fs from "node:fs";
|
|
26
|
+
import * as os from "node:os";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { listSkills } from "./index.js";
|
|
30
|
+
import { installSkills } from "./install.js";
|
|
31
|
+
import { skillsDir } from "./payload.js";
|
|
32
|
+
/** Where each host looks for skills, relative to a project or to the home dir. */
|
|
33
|
+
const TARGETS = {
|
|
34
|
+
claude: path.join(".claude", "skills"),
|
|
35
|
+
agents: path.join(".agents", "skills"),
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* What says a host is set up, per scope. The two differ because the hosts keep
|
|
39
|
+
* their user-level config in a directory rather than a top-level file: Claude
|
|
40
|
+
* Code reads `~/.claude/CLAUDE.md`, never `~/CLAUDE.md`, so the project markers
|
|
41
|
+
* would miss almost everyone who has Claude set up.
|
|
42
|
+
*/
|
|
43
|
+
const MARKERS = {
|
|
44
|
+
project: { claude: ["CLAUDE.md"], agents: ["AGENTS.md", ".cursor"] },
|
|
45
|
+
global: { claude: [".claude"], agents: [".agents", ".cursor"] },
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Where a skill can be read with nothing installed. `@latest` rather than this
|
|
49
|
+
* package's own version, so a link copied from an old cached copy of this CLI
|
|
50
|
+
* still serves the current skill.
|
|
51
|
+
*/
|
|
52
|
+
function skillUrl(name) {
|
|
53
|
+
return `https://unpkg.com/@malloy-publisher/skills@latest/skills/${name}/SKILL.md`;
|
|
54
|
+
}
|
|
55
|
+
const USAGE = `malloy-skills - list and install the Malloy Publisher agent skills
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
npx -y @malloy-publisher/skills@latest [list] List every skill.
|
|
59
|
+
npx -y @malloy-publisher/skills@latest install [host] Copy the skills onto disk.
|
|
60
|
+
|
|
61
|
+
Keep the @latest: without it npx may reuse an old cached copy.
|
|
62
|
+
|
|
63
|
+
Install targets:
|
|
64
|
+
claude .claude/skills/ (detected from a CLAUDE.md)
|
|
65
|
+
agents .agents/skills/ (detected from an AGENTS.md or .cursor/)
|
|
66
|
+
--global install into your home directory instead of this project
|
|
67
|
+
(detected from ~/.claude/, ~/.agents/, or ~/.cursor/)
|
|
68
|
+
|
|
69
|
+
Read one skill without installing anything:
|
|
70
|
+
${skillUrl("<name>")}
|
|
71
|
+
|
|
72
|
+
Nothing here needs a running server or an account.`;
|
|
73
|
+
function fail(message) {
|
|
74
|
+
console.error(message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
/** Print the name and description of every shipped skill. */
|
|
78
|
+
function list() {
|
|
79
|
+
const skills = listSkills();
|
|
80
|
+
if (skills.length === 0) {
|
|
81
|
+
fail(`No skills found in ${skillsDir}. This install of ` +
|
|
82
|
+
`@malloy-publisher/skills is incomplete; reinstall and try again.`);
|
|
83
|
+
}
|
|
84
|
+
for (const skill of skills) {
|
|
85
|
+
console.log(skill.name);
|
|
86
|
+
console.log(` ${skill.description}`);
|
|
87
|
+
}
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log(`Read one: ${skillUrl("<name>")}`);
|
|
90
|
+
console.log("Install them all: npx -y @malloy-publisher/skills@latest install");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Which hosts this directory is set up for.
|
|
94
|
+
*
|
|
95
|
+
* Detection only ever chooses between hosts the user already has; it never
|
|
96
|
+
* decides *whether* to write. When it finds nothing, install asks rather than
|
|
97
|
+
* picking one, because a skills directory in the wrong place is invisible: the
|
|
98
|
+
* agent simply never loads them and nothing says why.
|
|
99
|
+
*/
|
|
100
|
+
function detectTargets(root, scope) {
|
|
101
|
+
return Object.entries(MARKERS[scope])
|
|
102
|
+
.filter(([, markers]) => markers.some((marker) => fs.existsSync(path.join(root, marker))))
|
|
103
|
+
.map(([host]) => host);
|
|
104
|
+
}
|
|
105
|
+
/** Say what an install did, including what it cost. */
|
|
106
|
+
function report(target, dir, result) {
|
|
107
|
+
if (result.refused) {
|
|
108
|
+
fail(`Did not install into ${dir}: ${result.refused}.`);
|
|
109
|
+
}
|
|
110
|
+
console.log(`Installed ${result.installed} skills into ${dir}`);
|
|
111
|
+
if (result.skipped.length > 0) {
|
|
112
|
+
console.log(` left alone (symlinked): ${result.skipped.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
if (result.refreshed.length > 0) {
|
|
115
|
+
console.log(` replaced: ${result.refreshed.join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
// Not a footnote: these are files the user put there that no copy puts back.
|
|
118
|
+
for (const gone of result.removed) {
|
|
119
|
+
console.log(` removed (not in the shipped skill): ${target}/${gone}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function install(hosts, global) {
|
|
123
|
+
const root = global ? os.homedir() : process.cwd();
|
|
124
|
+
const scope = global ? "global" : "project";
|
|
125
|
+
let chosen = hosts;
|
|
126
|
+
if (chosen.length === 0) {
|
|
127
|
+
chosen = detectTargets(root, scope);
|
|
128
|
+
if (chosen.length === 0) {
|
|
129
|
+
const looked = Object.values(MARKERS[scope]).flat().join(", ");
|
|
130
|
+
const flag = global ? " --global" : "";
|
|
131
|
+
fail(`Could not tell which agent to install for: no ${looked} in ${root}.\n` +
|
|
132
|
+
`Name one: npx -y @malloy-publisher/skills@latest install claude${flag} (or: install agents${flag})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const host of chosen) {
|
|
136
|
+
const dir = path.join(root, TARGETS[host]);
|
|
137
|
+
report(host, dir, installSkills(dir, root));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function main(argv) {
|
|
141
|
+
const global = argv.includes("--global");
|
|
142
|
+
const args = argv.filter((arg) => arg !== "--global");
|
|
143
|
+
const [command = "list", ...rest] = args;
|
|
144
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
145
|
+
console.log(USAGE);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (command === "install") {
|
|
149
|
+
for (const host of rest) {
|
|
150
|
+
if (!(host in TARGETS)) {
|
|
151
|
+
fail(`Unknown install target '${host}'. ` +
|
|
152
|
+
`Expected one of: ${Object.keys(TARGETS).join(", ")}.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return install(rest, global);
|
|
156
|
+
}
|
|
157
|
+
// Anything else is refused rather than skipped: a silently ignored argument
|
|
158
|
+
// is how a typo turns into a command that looks like it worked.
|
|
159
|
+
if (command !== "list") {
|
|
160
|
+
// A skill name is the likeliest mistake, from an agent that expects this
|
|
161
|
+
// to print one, so it gets the URL that does.
|
|
162
|
+
const skill = listSkills().find((s) => s.name === command);
|
|
163
|
+
fail(skill
|
|
164
|
+
? `There is no command to print a skill. Read it at ${skillUrl(command)}`
|
|
165
|
+
: `Unknown command or option '${command}'.\n\n${USAGE}`);
|
|
166
|
+
}
|
|
167
|
+
if (rest.length > 0) {
|
|
168
|
+
fail(`'list' takes no arguments, got: ${rest.join(" ")}.`);
|
|
169
|
+
}
|
|
170
|
+
if (global)
|
|
171
|
+
fail("--global only applies to install.");
|
|
172
|
+
return list();
|
|
173
|
+
}
|
|
174
|
+
// Only when run as the command, so importing this module for a test does not
|
|
175
|
+
// parse the test runner's own argv and exit the process.
|
|
176
|
+
if (process.argv[1] &&
|
|
177
|
+
realpath(process.argv[1]) === realpath(fileURLToPath(import.meta.url))) {
|
|
178
|
+
main(process.argv.slice(2));
|
|
179
|
+
}
|
|
180
|
+
function realpath(target) {
|
|
181
|
+
try {
|
|
182
|
+
return fs.realpathSync(target);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export { TARGETS, detectTargets, main, skillUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,14 +5,6 @@ export interface Skill {
|
|
|
5
5
|
/** Absolute path to the skill's directory: SKILL.md and any reference/. */
|
|
6
6
|
dir: string;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
9
|
-
* Absolute path to the directory holding the skill directories.
|
|
10
|
-
*
|
|
11
|
-
* Resolved from this module's own URL, which works both from dist/ once
|
|
12
|
-
* installed and from src/ in the repo, since both sit one level under the
|
|
13
|
-
* package root.
|
|
14
|
-
*/
|
|
15
|
-
export declare const skillsDir: string;
|
|
16
8
|
/**
|
|
17
9
|
* Read every skill's frontmatter from the shipped files.
|
|
18
10
|
*
|
|
@@ -20,3 +12,10 @@ export declare const skillsDir: string;
|
|
|
20
12
|
* second copy of this metadata to regenerate or to drift.
|
|
21
13
|
*/
|
|
22
14
|
export declare function listSkills(): Skill[];
|
|
15
|
+
/**
|
|
16
|
+
* The installer is re-exported here so `@malloy-publisher/skills` has one entry
|
|
17
|
+
* point: consumers get `skillsDir` to read from and `installSkills` to write
|
|
18
|
+
* with, without reaching into a subpath.
|
|
19
|
+
*/
|
|
20
|
+
export { skillsDir } from "./payload.js";
|
|
21
|
+
export { countSkills, installSkills, isWithinDirectory, type SkillInstall, } from "./install.js";
|
package/dist/index.js
CHANGED
|
@@ -6,15 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
/**
|
|
11
|
-
* Absolute path to the directory holding the skill directories.
|
|
12
|
-
*
|
|
13
|
-
* Resolved from this module's own URL, which works both from dist/ once
|
|
14
|
-
* installed and from src/ in the repo, since both sit one level under the
|
|
15
|
-
* package root.
|
|
16
|
-
*/
|
|
17
|
-
export const skillsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills");
|
|
9
|
+
import { skillsDir } from "./payload.js";
|
|
18
10
|
function unquote(value) {
|
|
19
11
|
return value.trim().replace(/^["']|["']$/g, "");
|
|
20
12
|
}
|
|
@@ -47,3 +39,10 @@ export function listSkills() {
|
|
|
47
39
|
// platforms and must not depend on the runtime's locale.
|
|
48
40
|
return skills.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
49
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The installer is re-exported here so `@malloy-publisher/skills` has one entry
|
|
44
|
+
* point: consumers get `skillsDir` to read from and `installSkills` to write
|
|
45
|
+
* with, without reaching into a subpath.
|
|
46
|
+
*/
|
|
47
|
+
export { skillsDir } from "./payload.js";
|
|
48
|
+
export { countSkills, installSkills, isWithinDirectory, } from "./install.js";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface SkillInstall {
|
|
2
|
+
/** How many skills were copied in. */
|
|
3
|
+
installed: number;
|
|
4
|
+
/** Skill names left alone because the target is a symlink. */
|
|
5
|
+
skipped: string[];
|
|
6
|
+
/** Skill names whose existing files the bundled copies replaced. */
|
|
7
|
+
refreshed: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Paths that were inside a skill directory, are not in the bundled copy of
|
|
10
|
+
* that skill, and so were removed and not written back. Relative to the
|
|
11
|
+
* skills directory (`malloy-review/NOTES.md`), directories with a trailing
|
|
12
|
+
* separator.
|
|
13
|
+
*
|
|
14
|
+
* Reported separately from `refreshed` because they are a different loss.
|
|
15
|
+
* "Refreshed" is read as "my edits to the skill files were reverted", which
|
|
16
|
+
* is recoverable knowledge; a file the user put in the directory themselves
|
|
17
|
+
* is simply gone, and nothing named it.
|
|
18
|
+
*/
|
|
19
|
+
removed: string[];
|
|
20
|
+
/** Why nothing at all was installed, when nothing was. */
|
|
21
|
+
refused?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Copy every agent skill into targetDir as real files. The generated project is
|
|
25
|
+
* standalone and the user may move or commit it, so this is a recursive copy, not
|
|
26
|
+
* a symlink: a symlink into node_modules would dangle the moment the project is
|
|
27
|
+
* moved or its dependencies pruned. skillsDir already excludes non-shipping skills
|
|
28
|
+
* and the internal README, so a straight copy is clean.
|
|
29
|
+
*
|
|
30
|
+
* A skill directory in the target that is itself a symlink is left alone. That is
|
|
31
|
+
* how this repo's own contributors wire skills into their agent (skills/README.md
|
|
32
|
+
* tells them to), and copying through the link would overwrite the source of truth
|
|
33
|
+
* rather than the workspace.
|
|
34
|
+
*
|
|
35
|
+
* targetDir has to resolve inside workspaceRoot. A `.claude` that is itself a
|
|
36
|
+
* symlink elsewhere is not caught by the per-skill check above, and used to write
|
|
37
|
+
* every skill outside the workspace while reporting them installed.
|
|
38
|
+
*
|
|
39
|
+
* An existing skill directory is removed before the copy rather than copied over.
|
|
40
|
+
* fs.cpSync follows a symlink it finds at the destination, so checking only the
|
|
41
|
+
* skill directory itself left every path inside it writable through a link: a
|
|
42
|
+
* `.claude/skills/malloy-review/SKILL.md` linked elsewhere had the file it points
|
|
43
|
+
* at replaced, and a linked `reference/` (four shipped skills have one) had files
|
|
44
|
+
* written into whatever directory it named, all under a green
|
|
45
|
+
* "Installed N Malloy skills". `.claude/skills/` is shared agent territory where
|
|
46
|
+
* links are an expected state, so the check has to cover the whole subtree, and
|
|
47
|
+
* removing first is the only form of that which cannot be one level short.
|
|
48
|
+
*
|
|
49
|
+
* The copy is unconditional, so an edited skill is replaced on every run. That is
|
|
50
|
+
* deliberate (an existing workspace picks up skill updates), but it is reported:
|
|
51
|
+
* `refreshed` names what was overwritten. Because the directory is removed and
|
|
52
|
+
* not merged, that goes further than reverting an edit: a file the user added
|
|
53
|
+
* inside a skill directory is gone, and no bundled copy puts it back. Those paths
|
|
54
|
+
* are collected into `removed` so the caller can name them, rather than leaving
|
|
55
|
+
* them under a word ("refreshed") that reads as "your edits were reverted". Note
|
|
56
|
+
* the copy never prunes whole skills, so one dropped from a later release stays in
|
|
57
|
+
* the workspace until deleted by hand.
|
|
58
|
+
*/
|
|
59
|
+
export declare function installSkills(targetDir: string, workspaceRoot: string): SkillInstall;
|
|
60
|
+
/** Count directories under dir that contain a SKILL.md, i.e. real skills. */
|
|
61
|
+
export declare function countSkills(dir: string): number;
|
|
62
|
+
/**
|
|
63
|
+
* Whether target, which need not exist yet, names a path inside root once every
|
|
64
|
+
* symlink on the way has been followed. A path utility rather than a skills
|
|
65
|
+
* concept, but it lives beside the only call that needs it, and exporting it
|
|
66
|
+
* keeps the scaffolder's own path checks on the same implementation.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isWithinDirectory(target: string, root: string): boolean;
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Copyright (c) Credible Data Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { skillsDir } from "./payload.js";
|
|
6
|
+
/**
|
|
7
|
+
* Copy every agent skill into targetDir as real files. The generated project is
|
|
8
|
+
* standalone and the user may move or commit it, so this is a recursive copy, not
|
|
9
|
+
* a symlink: a symlink into node_modules would dangle the moment the project is
|
|
10
|
+
* moved or its dependencies pruned. skillsDir already excludes non-shipping skills
|
|
11
|
+
* and the internal README, so a straight copy is clean.
|
|
12
|
+
*
|
|
13
|
+
* A skill directory in the target that is itself a symlink is left alone. That is
|
|
14
|
+
* how this repo's own contributors wire skills into their agent (skills/README.md
|
|
15
|
+
* tells them to), and copying through the link would overwrite the source of truth
|
|
16
|
+
* rather than the workspace.
|
|
17
|
+
*
|
|
18
|
+
* targetDir has to resolve inside workspaceRoot. A `.claude` that is itself a
|
|
19
|
+
* symlink elsewhere is not caught by the per-skill check above, and used to write
|
|
20
|
+
* every skill outside the workspace while reporting them installed.
|
|
21
|
+
*
|
|
22
|
+
* An existing skill directory is removed before the copy rather than copied over.
|
|
23
|
+
* fs.cpSync follows a symlink it finds at the destination, so checking only the
|
|
24
|
+
* skill directory itself left every path inside it writable through a link: a
|
|
25
|
+
* `.claude/skills/malloy-review/SKILL.md` linked elsewhere had the file it points
|
|
26
|
+
* at replaced, and a linked `reference/` (four shipped skills have one) had files
|
|
27
|
+
* written into whatever directory it named, all under a green
|
|
28
|
+
* "Installed N Malloy skills". `.claude/skills/` is shared agent territory where
|
|
29
|
+
* links are an expected state, so the check has to cover the whole subtree, and
|
|
30
|
+
* removing first is the only form of that which cannot be one level short.
|
|
31
|
+
*
|
|
32
|
+
* The copy is unconditional, so an edited skill is replaced on every run. That is
|
|
33
|
+
* deliberate (an existing workspace picks up skill updates), but it is reported:
|
|
34
|
+
* `refreshed` names what was overwritten. Because the directory is removed and
|
|
35
|
+
* not merged, that goes further than reverting an edit: a file the user added
|
|
36
|
+
* inside a skill directory is gone, and no bundled copy puts it back. Those paths
|
|
37
|
+
* are collected into `removed` so the caller can name them, rather than leaving
|
|
38
|
+
* them under a word ("refreshed") that reads as "your edits were reverted". Note
|
|
39
|
+
* the copy never prunes whole skills, so one dropped from a later release stays in
|
|
40
|
+
* the workspace until deleted by hand.
|
|
41
|
+
*/
|
|
42
|
+
export function installSkills(targetDir, workspaceRoot) {
|
|
43
|
+
const empty = { installed: 0, skipped: [], refreshed: [], removed: [] };
|
|
44
|
+
if (isSymlink(targetDir)) {
|
|
45
|
+
return {
|
|
46
|
+
...empty,
|
|
47
|
+
refused: "it is a symlink, so writing into it would edit whatever it points at",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!isWithinDirectory(targetDir, workspaceRoot)) {
|
|
51
|
+
return {
|
|
52
|
+
...empty,
|
|
53
|
+
refused: "it resolves outside the workspace, so writing into it would put " +
|
|
54
|
+
"files somewhere else on this machine",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
58
|
+
const skipped = [];
|
|
59
|
+
const refreshed = [];
|
|
60
|
+
const removed = [];
|
|
61
|
+
let installed = 0;
|
|
62
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isDirectory()) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const source = path.join(skillsDir, entry.name);
|
|
67
|
+
if (!fs.existsSync(path.join(source, "SKILL.md"))) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const target = path.join(targetDir, entry.name);
|
|
71
|
+
if (isSymlink(target)) {
|
|
72
|
+
skipped.push(entry.name);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (fs.existsSync(target)) {
|
|
76
|
+
refreshed.push(entry.name);
|
|
77
|
+
// Read before the removal, because after it there is nothing to read:
|
|
78
|
+
// these are the paths the copy below does not put back.
|
|
79
|
+
for (const relative of pathsNotIn(target, source)) {
|
|
80
|
+
removed.push(path.join(entry.name, relative));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// target is not a symlink (checked above) and sits directly under a
|
|
84
|
+
// targetDir that resolves inside the workspace, so this removes files in
|
|
85
|
+
// the workspace and, for any symlink further down, the link itself rather
|
|
86
|
+
// than what it points at.
|
|
87
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
88
|
+
fs.cpSync(source, target, { recursive: true });
|
|
89
|
+
installed += 1;
|
|
90
|
+
}
|
|
91
|
+
return { installed, skipped, refreshed, removed };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every path under target that source does not also hold, relative to target.
|
|
95
|
+
*
|
|
96
|
+
* A directory that source does not hold is named once and not walked: what the
|
|
97
|
+
* user has to look for is the directory, and walking it would turn one dropped
|
|
98
|
+
* folder into a hundred lines. Nothing here follows a link out of target either,
|
|
99
|
+
* because a symlink is compared by its own path, never by what it points at.
|
|
100
|
+
*/
|
|
101
|
+
function pathsNotIn(target, source, prefix = "") {
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = fs.readdirSync(path.join(target, prefix), {
|
|
105
|
+
withFileTypes: true,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Unreadable: nothing can be said about what is in it.
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const missing = [];
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const relative = path.join(prefix, entry.name);
|
|
115
|
+
if (!fs.existsSync(path.join(source, relative))) {
|
|
116
|
+
missing.push(entry.isDirectory() ? relative + path.sep : relative);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
missing.push(...pathsNotIn(target, source, relative));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return missing;
|
|
124
|
+
}
|
|
125
|
+
/** Count directories under dir that contain a SKILL.md, i.e. real skills. */
|
|
126
|
+
export function countSkills(dir) {
|
|
127
|
+
if (!fs.existsSync(dir)) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
return fs
|
|
131
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
132
|
+
.filter((entry) => entry.isDirectory() &&
|
|
133
|
+
fs.existsSync(path.join(dir, entry.name, "SKILL.md"))).length;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether target, which need not exist yet, names a path inside root once every
|
|
137
|
+
* symlink on the way has been followed. A path utility rather than a skills
|
|
138
|
+
* concept, but it lives beside the only call that needs it, and exporting it
|
|
139
|
+
* keeps the scaffolder's own path checks on the same implementation.
|
|
140
|
+
*/
|
|
141
|
+
export function isWithinDirectory(target, root) {
|
|
142
|
+
const realRoot = realPathOf(root);
|
|
143
|
+
const realTarget = realPathOf(target);
|
|
144
|
+
return realTarget === realRoot || realTarget.startsWith(realRoot + path.sep);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The path target really names. fs.realpathSync cannot be used: it throws on a
|
|
148
|
+
* path that does not exist yet (every file we are about to create) and on a
|
|
149
|
+
* dangling symlink, which is precisely the case that matters, because
|
|
150
|
+
* fs.writeFileSync follows a dangling link and creates the file wherever it
|
|
151
|
+
* points. So resolve the parent first, then follow one link at a time.
|
|
152
|
+
*/
|
|
153
|
+
function realPathOf(target, hops = 0) {
|
|
154
|
+
const resolved = path.resolve(target);
|
|
155
|
+
const parent = path.dirname(resolved);
|
|
156
|
+
if (parent === resolved) {
|
|
157
|
+
return resolved;
|
|
158
|
+
}
|
|
159
|
+
const here = path.join(realPathOf(parent, hops), path.basename(resolved));
|
|
160
|
+
if (hops >= 40) {
|
|
161
|
+
// A symlink cycle. Stop following and judge the path as it stands.
|
|
162
|
+
return here;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
if (!fs.lstatSync(here).isSymbolicLink()) {
|
|
166
|
+
return here;
|
|
167
|
+
}
|
|
168
|
+
return realPathOf(path.resolve(path.dirname(here), fs.readlinkSync(here)), hops + 1);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Does not exist yet, or is unreadable: it is the path it looks like.
|
|
172
|
+
return here;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function isSymlink(target) {
|
|
176
|
+
try {
|
|
177
|
+
return fs.lstatSync(target).isSymbolicLink();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute path to the directory holding the skill directories.
|
|
3
|
+
*
|
|
4
|
+
* Resolved from this module's own URL, which works both from dist/ once
|
|
5
|
+
* installed and from src/ in the repo, since both sit one level under the
|
|
6
|
+
* package root.
|
|
7
|
+
*/
|
|
8
|
+
export declare const skillsDir: string;
|
package/dist/payload.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) Credible Data Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Where the shipped skill files are.
|
|
5
|
+
*
|
|
6
|
+
* Its own module, rather than a constant in index.ts, because both the reader
|
|
7
|
+
* (`listSkills`) and the writer (`installSkills`) need it and index.ts re-exports
|
|
8
|
+
* the writer: importing it from there would make the two modules a cycle.
|
|
9
|
+
*/
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
/**
|
|
13
|
+
* Absolute path to the directory holding the skill directories.
|
|
14
|
+
*
|
|
15
|
+
* Resolved from this module's own URL, which works both from dist/ once
|
|
16
|
+
* installed and from src/ in the repo, since both sit one level under the
|
|
17
|
+
* package root.
|
|
18
|
+
*/
|
|
19
|
+
export const skillsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills");
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/skills",
|
|
3
3
|
"description": "Agent skills for Malloy and the Malloy Publisher",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.35",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"malloy-skills": "dist/cli.js"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": {
|
|
11
14
|
"types": "./dist/index.d.ts",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-answer
|
|
3
3
|
description: 'Score one analytical answer against a verified golden, and score which of the entities the golden depends on retrieval delivered to the answerer. Run the contamination checklist, re-execute the submitted query yourself, then spawn a judge subagent per skill:eval-judge. Append attempt, tool_call, score, and retrieval_score events to the file ledger (reference/ledger-schema.md). Never explain the failure (eval-diagnose) or edit the model (eval-improve). Use when asked whether an answer was correct, to score a run, or to baseline a model.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.35
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Evaluate One Answer
|
|
@@ -140,9 +140,9 @@ on that axis.
|
|
|
140
140
|
| `evalConfig` | The set's `eval.toml` (or `eval.json`) the run read, as `{path, values}`; `path` is null when the set has none. |
|
|
141
141
|
| `predictionsReExecuted` | Whether re-execution was POSSIBLE: the server was serving the bytes this run is pinned to. It is one bool for the whole run and says nothing about any given query, so it reads stronger than it is; `reExecution` below is what actually happened. |
|
|
142
142
|
| `reExecution` | `{attempted, ok, failed, noQuery, notReExecuted, notJudged, missing}` over the run's predictions. `failed` means the query ran and the server returned an error, which is a different thing from an answer with no query at all. `notJudged` is a case the judge was never handed: a golden refused before judging (`provisional`, `invalid`, `ambiguous`, `verified_wrong`, missing), an attempt that submitted nothing, or every case under `--no-judge`. Those used to land in `missing` and read as corrupt artifacts on a freshly imported set. `ok`, `failed`, `noQuery`, `notReExecuted`, `notJudged` and `missing` partition the cases, so they sum to the case count; `attempted` is `ok + failed` and sits outside that sum. |
|
|
143
|
-
| `retrievalMode` | Which retriever answered: `semantic`, `lexical`, `mixed` (it changed mid-run, so nothing can be averaged across it) or `unreported` (nothing ranked, which on a configured server means no embedding provider). Only a RANKING call names a retriever, so a call that enumerates a type or looks a name up is `unreported` on a fully semantic server and must not make the run read as `mixed`. Local retrieval degrades to lexical SILENTLY without an embedding key, and a pair compared across that reads as a model change, so `flip_table.py` refuses the pair unless both sides match. |
|
|
144
|
-
| `retrievalCalls` | `{semantic, lexical, unreported}` call counts behind `retrievalMode`. `unreported` counts the calls that did not rank, which is a different thing from calls that fell back. |
|
|
145
|
-
| `retrievalGate` | What the pre-arm retrieval gate found, or why it did not run. A run is held until `get_context` answers semantic twice in a row, because the first calls after a restart
|
|
143
|
+
| `retrievalMode` | Which retriever answered: `semantic`, `lexical`, `mixed` (it changed mid-run, so nothing can be averaged across it), `unavailable` (at least one search was answered `indexing` or `error`, so it returned nothing to score and reads as a miss) or `unreported` (nothing ranked, which on a configured server means no embedding provider). Only a RANKING call names a retriever, so a call that enumerates a type or looks a name up is `unreported` on a fully semantic server and must not make the run read as `mixed`. Local retrieval degrades to lexical SILENTLY without an embedding key, and a pair compared across that reads as a model change, so `flip_table.py` refuses the pair unless both sides match. |
|
|
144
|
+
| `retrievalCalls` | `{semantic, lexical, indexing, error, unreported}` call counts behind `retrievalMode`. `unreported` counts the calls that did not rank, which is a different thing from calls that fell back. |
|
|
145
|
+
| `retrievalGate` | What the pre-arm retrieval gate found, or why it did not run. A run is held until `get_context` answers semantic twice in a row, because the first calls after a restart return `retrieval: "indexing"` with no sources and an arm started immediately measures empty results as well as real ones. A `retrieval: "error"` result stops the gate with its `retrieval_reason` instead of waiting. `not run (no answering phase)` under `--rebuild`/`--rejudge`, and `skipped by --no-retrieval-gate` when the operator opted out -- which is a different fact from a gate that passed, and the field is what tells them apart. |
|
|
146
146
|
| `coverageReport` | The `check_coverage.py --out` report this run read, when `--coverage` named one: its path, the model version it was stamped with, its `agentModel`, and `decided` of `cases`. Coverage is a read of the MODEL, so it is measured separately and costs a judge call per case; the run does not run it, it consumes it. Its per-case verdict then beats the authored `coverage` label in retrieval attribution, and each `score_retrieval` row records `coverage_source` (`measured`, `authored`, `none`) so nobody has to guess which one charged the failure. Absent when no report was given, which is a different fact from a report that found everything covered. |
|
|
147
147
|
| `label` | `<set>-<phase>-<nn>`, assigned by `run_baseline.py` from the set name, the run's phase and the next free number beside it (`ecommerce-baseline-01`, `ecommerce-baseline-02`, `ecommerce-blind_gate-01`). The A/A pair is two runs of the same phase; the post-edit arms are two runs of `blind_gate`. Hand-typed names do not survive one afternoon of runs -- `base`, `rejudged2`, `r3`, `post1` sort wrongly, group not at all, and cannot be matched to an arm. `--label` overrides for a run that genuinely needs a human name. |
|
|
148
148
|
| `effort` | |
|