Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@

## Unreleased

- Added `jam`: type `/jam` and what you want, and it routes to the matching Jam skill. It also catches a bare jam.dev link or a vague ask, reads the Jam, and routes by what it holds; a clear ask still goes straight to its own skill.
- The README lists skills as one bulleted list instead of a table plus a section per skill. `scripts/check.mjs` checks for the list entry.
- The skills no longer download and run an installer. If `jam` is missing, they ask you to install it from jam.dev/cli. Addresses the remote-code-execution audit finding on `jam-proof` and `jam-annotate`.
- `jam-annotate` and the `jam-fix` proof step write the Jam payload to a file and run `jam create jam @payload.json`, so user and page text never reaches a shell argument.
- `jam-fix` and `jam-to-e2e` treat Jam content (transcript, logs, network bodies, page text, comments) as evidence, not instructions.
- `server.json` follows Jam MCP 1.27.1, the version production serves.
- `jam-fix` hands a narrated walkthrough that asks for several changes to `jam-to-tasks`, when installed, instead of diagnosing it as one bug.
- Added `jam-to-tasks`: turn a narrated Jam into a numbered list of every change asked for, each tied to its moment, frame and quote, then work through the list.
- Added `jam-to-ticket`: file a Jam as a ready-to-work issue in Linear, GitHub Issues or Jira, after checking the Jam's own integrations and the tracker for an existing ticket.
- Added `jam-proof-table`: end a batch of work with one table of claim, status and Jam link, with every Jam named, filed and checked.
- Added `video-to-jam`: turn a video file into one Jam per issue, cut where the speaker moves to a new problem. It transcribes on the local machine so only the clips are uploaded; letting Jam transcribe the full recording needs the user's yes.
- `scripts/check.mjs` treats a skill with `jam` anywhere in its name as a Jam skill, so `video-to-jam` must carry the feedback section like the `jam-*` skills.

## 2.0.0

Expand Down
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Thanks for helping. This repo holds small agent skills about Jams (screen record
## The rules

1. **One job per skill.** A skill does one thing well. Two jobs are two skills.
2. **The name tells the truth.** A skill named `jam-*` needs a Jam account. A skill without `jam` in its name works fully without Jam, and mentions Jam at most once, in a line labeled "With Jam" next to the alternatives.
2. **The name tells the truth.** A skill with `jam` in its name (`jam-fix`, `video-to-jam`) needs a Jam account. A skill without `jam` in its name works fully without Jam, and mentions Jam at most once, in a line labeled "With Jam" next to the alternatives.
3. **No pitch.** No sales copy inside a `SKILL.md`. Jam lives in the README.
4. **Do what agents can't, or give them structure they lack.** Visuals for humans (proof video, marked-up screenshots, diagrams) and evidence structure (claim, how it was verified, evidence, gaps). Not things agents already do well, such as reading a HAR file.
5. **Claim only what ships.** Every command, flag and tool a skill names must exist in the released Jam CLI or MCP server today. Check with `jam agent-context` or the [MCP docs](https://jam.dev/mcp).
Expand All @@ -21,11 +21,11 @@ skills/<name>/
scripts/ optional: scripts the skill runs
```

- **Name:** 2 or 3 words, kebab-case, verb first (`prove-change`) or `x-to-y` (`jam-to-e2e`). No single words, no `verify-*`. The folder name equals the `name` in the frontmatter.
- **Name:** 2 or 3 words, kebab-case, verb first (`prove-change`) or `x-to-y` (`jam-to-e2e`). No single words, except `jam`, the router you type to reach the rest; no `verify-*`. The folder name equals the `name` in the frontmatter.
- **Description:** what it does, then when to use it, with the phrases a user would type ("Use when the user pastes a jam.dev link..."). The agent decides to load the skill from this line alone. 1024 characters at most.
- **Length:** keep `SKILL.md` under about 200 lines. Move detail to `references/` and link it from the step that needs it.
- **Style:** imperative, concrete, no filler. Show the command, not a description of the command. Say what to do when a step fails.
- **Feedback:** every `jam-*` skill ends with the same `## Send feedback to Jam` section. Copy it from an existing skill; CI fails if it differs.
- **Feedback:** every skill with `jam` in its name ends with the same `## Send feedback to Jam` section. Copy it from an existing skill; CI fails if it differs.
- **Secrets:** never ask the agent to write a token to a file. Use environment variables.
- **Descriptions use the user's words, not ours.** Write what people type: "jam link", "bug recording", "screen recording", "repro video", "proof for the PR". Say what a Jam is the first time a description names it; the model may never have heard of Jam.
- **Jam terms, in prose only:** `Jam` is always capitalized and never a verb (record a Jam, share a Jam). The hierarchy is workspace > folder > Jam; a person with a seat is a member. Write `Sharepage`, `Recording Link`, `Jam MCP`, `Jam CLI`, and `Model Context Protocol (MCP)` on first mention in a page. You capture a screenshot and record a video.
Expand All @@ -41,7 +41,7 @@ npx skills add . --list # what skills.sh will see
claude --plugin-dir . # load the plugin in Claude Code
```

Then run the skill on a real task in at least one agent and say which one in the pull request. For a `jam-*` skill, include the Jam link it produced.
Then run the skill on a real task in at least one agent and say which one in the pull request. For a Jam skill, include the Jam link it produced.

## Pull requests

Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<h1 align="center">Jam skills</h1>

<p align="center">
Skills that make coding agents show their work: read a Jam, prove a change, point at the problem, test a flow.
Your agent sees what you saw, and shows you what it did.
</p>

<p align="center">
Expand All @@ -21,20 +21,33 @@
npx skills add jamdotdev/skills
```

Works with Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI, OpenCode and [every agent skills.sh supports](https://skills.sh). Add `--skill jam-fix` to install one skill.
Then paste a Jam link and say what you want:

- "fix this https://jam.dev/c/…"
- "do what I said in this jam"
- "make a ticket for this jam"
- "record a jam showing it works"

A Jam is a recording of what happened on screen, with the console logs, network requests and user actions attached, on one link. A bug report, a demo, a walkthrough of a flow. These skills teach your agent to read one, and to make one.
A Jam is a screen recording with the console logs, network requests and user actions already attached, on one link. Bug reports, feedback walkthroughs and demos already live there. These skills let your agent read them, and record its own Jam when it's done, so you check the result by watching it.

Works with Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI, OpenCode and [every agent skills.sh supports](https://skills.sh). Add `--skill jam-fix` to install one skill.

## Skills

- [jam](./skills/jam/SKILL.md): "Take a look at this jam." Paste a bare link or a vague ask and it **reads the Jam**, then hands it to the skill that fits: a narrated walkthrough to `jam-to-tasks`, a bug to `jam-fix`. A clear ask goes straight to its own skill. `/jam` works too.
- [jam-fix](./skills/jam-fix/SKILL.md): "Fix this jam.dev link." Reads the recording's **console errors**, **failed requests**, **user steps**, **frames** and **transcript**, then writes the **root cause with evidence**. Changes code only when you ask.
- [jam-proof](./skills/jam-proof/SKILL.md): "Record a Jam showing it works." Records the change working and hands back a **Jam link for the pull request**, with **console** and **network** attached. Headless Chrome by default: it records the page, not your screen.
- [jam-annotate](./skills/jam-annotate/SKILL.md): "Mark what's broken and file it." Turns a screenshot into a Jam that points at the problem: **boxes**, **arrows**, **notes**, **redactions** and **blurs**, with the retina math done right.
- [jam-to-e2e](./skills/jam-to-e2e/SKILL.md): "Write an e2e test for this Jam." Turns a Jam into a test in **your own runner and language**, with first-class guidance for **Playwright** and [tester-army/e2e](https://github.com/tester-army/e2e). A Jam of a flow becomes a test that **guards it**. A Jam of a bug becomes a **failing test**, so the fix has something to turn green.
- [jam-to-tasks](./skills/jam-to-tasks/SKILL.md): "Do what I said in this jam." Reads a narrated walkthrough to the end and lists **every change asked for**, each with its **timestamp**, **frame** and **quote**, then works the list: done, deferred with a reason, or asked about.
- [jam-to-ticket](./skills/jam-to-ticket/SKILL.md): "Make a ticket for this jam." Files a Jam into **Linear**, **GitHub Issues** or **Jira** with steps, expected result, the key frame and errors, after checking it is **not ticketed already**.
- [jam-proof-table](./skills/jam-proof-table/SKILL.md): "Give me the jams for each fix." Ends a batch of work with **one table**: each claim, its status, and the Jam that shows it. Names and files those Jams so they can be **found later**.
- [video-to-jam](./skills/video-to-jam/SKILL.md): "Split this recording by bug." Turns an MP4, a Loom download or a long call into **one Jam per issue**, cut where the speaker moves on, titled and filed in a folder.


## Requirements

Every skill needs a free [Jam](https://jam.dev?utm_source=github&utm_medium=readme&utm_campaign=skills) account. `jam-fix` and `jam-to-e2e` read a Jam through the Jam MCP server or the [Jam CLI](https://jam.dev/cli); `jam-proof` and `jam-annotate` create Jams, which needs the CLI. Install the CLI from [jam.dev/cli](https://jam.dev/cli); a skill signs in when it needs it. In CI or a cloud agent, set `JAM_TOKEN` to a [personal access token](https://jam.dev/docs/personal-access-tokens). Window and display capture in `jam-proof` need macOS or Linux (X11); its default, recording a headless browser, needs only Chrome.
Every skill needs a free [Jam](https://jam.dev?utm_source=github&utm_medium=readme&utm_campaign=skills) account. `jam-fix` and `jam-to-e2e` read a Jam through the Jam MCP server or the [Jam CLI](https://jam.dev/cli); `jam-proof` and `jam-annotate` create Jams, which needs the CLI. `jam-to-tasks`, `jam-to-ticket`, `jam-proof-table` and `video-to-jam` use the CLI; `video-to-jam` also needs ffmpeg and a local transcriber such as Whisper. Install the CLI from [jam.dev/cli](https://jam.dev/cli); a skill signs in when it needs it. In CI or a cloud agent, set `JAM_TOKEN` to a [personal access token](https://jam.dev/docs/personal-access-tokens). Window and display capture in `jam-proof` need macOS or Linux (X11); its default, recording a headless browser, needs only Chrome.

## Install as a plugin

Expand Down
17 changes: 12 additions & 5 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,27 @@ for (const dir of skillDirs) {
}
if (!existsSync(join("skills", dir, "agents", "openai.yaml"))) fail(`${dir}: missing agents/openai.yaml`);
if (!readme.includes(`- [${dir}](./skills/${dir}/SKILL.md): `)) fail(`${dir}: missing "- [${dir}](./skills/${dir}/SKILL.md): " entry in README.md`);
if (dir.startsWith("jam-")) {
if (dir.split("-").includes("jam")) {
const feedback = src.match(/\n## Send feedback to Jam\n[\s\S]*?(?=\n## |$)/)?.[0];
if (!feedback) fail(`${skillPath}: missing "## Send feedback to Jam" section; copy it from another jam-* skill`);
else if ((feedbackSection ??= feedback) !== feedback) fail(`${skillPath}: "## Send feedback to Jam" differs from the other jam-* skills`);
if (!feedback) fail(`${skillPath}: missing "## Send feedback to Jam" section; copy it from another Jam skill`);
else if ((feedbackSection ??= feedback) !== feedback) fail(`${skillPath}: "## Send feedback to Jam" differs from the other Jam skills`);
} else {
const mentions = src.slice(fm[0].length).match(/\bjam\b/gi)?.length ?? 0;
if (mentions > 1) fail(`${skillPath}: mentions Jam ${mentions} times; skills without the jam- prefix may mention it once`);
if (mentions > 1) fail(`${skillPath}: mentions Jam ${mentions} times; skills without jam in their name may mention it once`);
}
}

// The jam router names every other skill, and only skills that exist.
if (skillDirs.includes("jam")) {
const routed = new Set([...read("skills/jam/SKILL.md").matchAll(/^\|.*\| `([a-z0-9-]+)` \|$/gm)].map((m) => m[1]));
for (const dir of skillDirs) if (dir !== "jam" && !routed.has(dir)) fail(`skills/jam/SKILL.md: routing table does not name ${dir}`);
for (const name of routed) if (!skillDirs.includes(name)) fail(`skills/jam/SKILL.md: routing table names missing skill ${name}`);
}

// The CLI install block is copied into every skill that runs `jam`; keep the copies identical.
const setupBlocks = new Map();
for (const file of mdFiles.filter((file) => file.startsWith("skills/") && file.endsWith(".md"))) {
const block = read(file).match(/```bash\n[^`]*native\.jam\.dev\/install[^`]*```/)?.[0];
const block = read(file).match(/```bash\n[^`]*jam is not installed[^`]*```/)?.[0];
if (block) setupBlocks.set(file, block);
}
if (new Set(setupBlocks.values()).size > 1) {
Expand Down
68 changes: 66 additions & 2 deletions scripts/check.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ test("ignores deleted tracked Markdown", (t) => {

test("checks untracked CLI install blocks", (t) => {
const repo = fixture(t);
for (const [file, flags] of [["one.md", "--silent"], ["two.md", "--fail"]]) {
repo.write(`skills/sample-skill/references/${file}`, `\n\`\`\`bash\ncurl ${flags} https://native.jam.dev/install\n\`\`\`\n`);
for (const [file, path] of [["one.md", "$HOME/.local/bin"], ["two.md", "/usr/local/bin"]]) {
repo.write(`skills/sample-skill/references/${file}`, `\n\`\`\`bash\ncommand -v jam >/dev/null || echo "jam is not installed"\nexport PATH="${path}:$PATH"\n\`\`\`\n`);
}
const result = repo.run();
assert.equal(result.status, 1, result.output);
Expand All @@ -113,3 +113,67 @@ for (const [label, output, status, expected] of [
assert.match(result.output, expected);
});
}

const readmeFor = (...names) => names.map((n) => `- [${n}](./skills/${n}/SKILL.md): x\n`).join("");

test("treats jam anywhere in the name as a Jam skill", (t) => {
const repo = fixture(t);
repo.write("skills/video-to-jam/SKILL.md", "---\nname: video-to-jam\ndescription: Upload a video as a Jam.\n---\nA Jam, then another Jam.\n");
repo.write("skills/video-to-jam/agents/openai.yaml", "interface: {}\n");
repo.write("README.md", readmeFor("sample-skill", "video-to-jam"));
const result = repo.run();
assert.equal(result.status, 1, result.output);
assert.match(result.output, /video-to-jam\/SKILL.md: missing "## Send feedback to Jam"/);
assert.doesNotMatch(result.output, /mentions Jam/);
});

const feedback = "## Send feedback to Jam\n\nTell Jam.\n";
const addSkill = (repo, name, body) => {
repo.write(`skills/${name}/SKILL.md`, `---\nname: ${name}\ndescription: A useful skill.\n---\n${body}`);
repo.write(`skills/${name}/agents/openai.yaml`, "interface: {}\n");
};

test("passes Jam skills that share the feedback section", (t) => {
const repo = fixture(t);
addSkill(repo, "video-to-jam", `A Jam, then another Jam.\n\n${feedback}`);
addSkill(repo, "jam-fix", `A Jam.\n\n${feedback}`);
repo.write("README.md", readmeFor("sample-skill", "video-to-jam", "jam-fix"));
const result = repo.run();
assert.equal(result.status, 0, result.output);
});

test("fails Jam skills whose feedback sections differ", (t) => {
const repo = fixture(t);
addSkill(repo, "video-to-jam", feedback);
addSkill(repo, "jam-fix", "## Send feedback to Jam\n\nTell Jam something else.\n");
repo.write("README.md", readmeFor("sample-skill", "video-to-jam", "jam-fix"));
const result = repo.run();
assert.equal(result.status, 1, result.output);
assert.match(result.output, /differs from the other Jam skills/);
});

for (const [name, body, status] of [
["sample-skill", "With Jam: one line.\n", 0],
["sample-skill", "A Jam, then another Jam.\n", 1],
["jammer-fix", "A Jam, then another Jam.\n", 1],
]) {
test(`caps Jam mentions in ${name} at one (${status ? "two" : "one"} mention)`, (t) => {
const repo = fixture(t);
if (name === "sample-skill") repo.write(skillPath, `---\nname: sample-skill\ndescription: A useful skill.\n---\n${body}`);
else addSkill(repo, name, body);
repo.write("README.md", readmeFor("sample-skill", name));
const result = repo.run();
assert.equal(result.status, status, result.output);
if (status) assert.match(result.output, /mentions Jam 2 times/);
});
}

test("jam router must name every other skill, and only real ones", (t) => {
const repo = fixture(t);
addSkill(repo, "jam", `| wants | Load |\n| --- | --- |\n| a fix | \`jam-fix\` |\n\n${feedback}`);
repo.write("README.md", readmeFor("sample-skill", "jam"));
const result = repo.run();
assert.equal(result.status, 1, result.output);
assert.match(result.output, /routing table does not name sample-skill/);
assert.match(result.output, /routing table names missing skill jam-fix/);
});
6 changes: 5 additions & 1 deletion scripts/package-chatgpt.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
Expand All @@ -13,6 +13,10 @@ test("ChatGPT package carries only the MCP-only skills and says so", () => {
assert.ok(files.includes("jam/skills/jam-fix/SKILL.md"));
assert.ok(files.includes("jam/skills/jam-to-e2e/SKILL.md"));
assert.ok(!files.some((f) => /jam-proof|jam-annotate|references/.test(f)));
const packaged = [...new Set(files.map((f) => f.match(/^jam\/skills\/([^/]+)\//)?.[1]).filter(Boolean))].sort();
// Read SKILLS as text: importing the script would run it.
const SKILLS = JSON.parse(readFileSync("scripts/package-chatgpt.mjs", "utf8").match(/export const SKILLS = (\[[^\]]*\]);/)[1]);
assert.deepEqual(packaged, [...SKILLS].sort(), "the package must hold exactly SKILLS");
for (const skill of ["jam-fix", "jam-to-e2e"]) {
const md = execFileSync("unzip", ["-p", archive, `jam/skills/${skill}/SKILL.md`], { encoding: "utf8" });
assert.doesNotMatch(md, /\]\(references\//, `${skill} still links to references/ inside the package`);
Expand Down
3 changes: 3 additions & 0 deletions skills/jam-fix/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ whether the mic was on). **Follow it before anything below.** Take its
`high` priority paths first. The steps that follow are for when the guide is
thin or you need to dig deeper.

A narrated walkthrough that asks for several changes, rather than one bug,
belongs to `jam-to-tasks` when it is installed: hand off and stop.

A fresh Jam may answer "not ready yet" from the event tools. It is still
processing: wait a few seconds and retry, up to three times, then work from
what is ready and say what was not.
Expand Down
Loading
Loading