diff --git a/README.md b/README.md index fa9805507d..d6b43e4ce7 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Try a prompt like: > Using `/hyperframes`, create a 10-second product intro with a fade-in title, a background video, and subtle background music. -The skills teach agents the HyperFrames production loop: plan the video, write valid HTML, wire seekable animations, add media, lint, preview, and render. They work with Claude Code, Cursor, Gemini CLI, Codex, and other coding agents that support skills. +The skills teach agents the HyperFrames production loop: plan the video, write valid HTML, wire seekable animations, add media, lint, preview, and render. They work with IBM Bob, Claude Code, Cursor, Gemini CLI, Codex, and other coding agents that support skills. ## Skills diff --git a/docs/guides/skills.mdx b/docs/guides/skills.mdx index f2e14aaadb..7aebefd89d 100644 --- a/docs/guides/skills.mdx +++ b/docs/guides/skills.mdx @@ -52,6 +52,18 @@ Install or refresh one workflow explicitly: npx hyperframes skills update pr-to-video ``` +## Use the skills in IBM Bob + +Run `npx hyperframes skills update`, then open or restart Bob Shell in the +project folder so it discovers the skills linked under `~/.bob/skills`: + +```bash +bob +``` + +Ask Bob to use `/hyperframes` for the first request. Running the update again +refreshes the same global skill bundles without changing the project format. + ## Use the skills in Google Antigravity Install the skills from the root of the project you open in Antigravity: diff --git a/packages/cli/src/telemetry/agent_runtime.test.ts b/packages/cli/src/telemetry/agent_runtime.test.ts index 3bd669d253..3b98b762e4 100644 --- a/packages/cli/src/telemetry/agent_runtime.test.ts +++ b/packages/cli/src/telemetry/agent_runtime.test.ts @@ -6,6 +6,9 @@ import { HINT_KEY_PATTERN } from "./agent_runtime.js"; // detectSandboxRuntime is exercised through a small set of node:os mocks. const VENDOR_ENV_KEYS = [ + "BOBSHELL_CLI", + "BOBSHELL_API_KEY", + "BOBSHELL_NO_RELAUNCH", "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CODEX_THREAD_ID", @@ -66,6 +69,35 @@ describe("detectAgentRuntime — base behavior", () => { }); }); +describe("detectAgentRuntime — IBM Bob Shell", () => { + const savedEnv = { ...process.env }; + beforeEach(stripVendorEnv); + afterEach(() => { + process.env = { ...savedEnv }; + }); + + it("detects via BOBSHELL_CLI (set on both shell execution paths)", async () => { + process.env["BOBSHELL_CLI"] = "1"; + const { detectAgentRuntime } = await import("./agent_runtime.js"); + expect(detectAgentRuntime()).toBe("bob"); + }); + + it("wins over inherited outer-agent markers as the immediate executor", async () => { + process.env["BOBSHELL_CLI"] = "1"; + process.env["CLAUDECODE"] = "1"; + process.env["CODEX_THREAD_ID"] = "thread-1"; + const { detectAgentRuntime } = await import("./agent_runtime.js"); + expect(detectAgentRuntime()).toBe("bob"); + }); + + it("does not treat Bob authentication or relaunch state as execution proof", async () => { + process.env["BOBSHELL_API_KEY"] = "secret-not-read"; + process.env["BOBSHELL_NO_RELAUNCH"] = "true"; + const { detectAgentRuntime } = await import("./agent_runtime.js"); + expect(detectAgentRuntime()).toBeNull(); + }); +}); + describe("detectAgentRuntime — Claude Code", () => { const savedEnv = { ...process.env }; beforeEach(stripVendorEnv); diff --git a/packages/cli/src/telemetry/agent_runtime.ts b/packages/cli/src/telemetry/agent_runtime.ts index 8cd40a246d..12c2dd90bb 100644 --- a/packages/cli/src/telemetry/agent_runtime.ts +++ b/packages/cli/src/telemetry/agent_runtime.ts @@ -22,6 +22,7 @@ import { detectWSL } from "./platform.js"; export type SandboxRuntime = "gvisor" | "firecracker" | "docker" | "kvm" | "wsl" | null; export type AgentRuntime = + | "bob" | "claude_code" | "codex" | "cursor" @@ -47,6 +48,18 @@ interface VendorRule { // before more generic ones (e.g. copilot_agent before a hypothetical generic // 'github_actions' rule). const VENDOR_RULES: VendorRule[] = [ + // IBM Bob Shell — both shell execution paths stamp BOBSHELL_CLI=1 onto the + // child environment: childProcessFallback's spawn env and executeWithPty's + // PTY env. Keep this ahead of other agents because Bob inherits the parent + // environment, so a Bob session launched from Codex or Claude can carry both + // vendors' markers; BOBSHELL_CLI identifies the immediate executor. + // BOBSHELL_API_KEY is deliberately ignored: it is an authentication setting, + // not proof that Bob launched the current process. + // Source: IBM Bob Shell 1.0.6 bundle (lb.childProcessFallback / executeWithPty). + { + name: "bob", + check: (env) => typeof env["BOBSHELL_CLI"] === "string", + }, // Anthropic Claude Code — sets CLAUDECODE=1 on every Bash/PowerShell tool // spawn (Shell.ts:321) and CLAUDE_CODE_ENTRYPOINT at startup, inherited by // every child (main.tsx:527). Both propagate to spawned subprocesses. diff --git a/packages/cli/src/utils/skillsMirror.test.ts b/packages/cli/src/utils/skillsMirror.test.ts index ae5d60f967..06dccbb033 100644 --- a/packages/cli/src/utils/skillsMirror.test.ts +++ b/packages/cli/src/utils/skillsMirror.test.ts @@ -119,6 +119,7 @@ describe("mirrorGlobalSkills", () => { const home = makeHome(); seedStore(home, ["hyperframes", "hyperframes-core"]); installMarker(home, ".cursor"); // cursor present + installMarker(home, ".bob"); // IBM Bob present installMarker(home, ".config/goose"); // goose present (XDG base) // windsurf NOT installed (no ~/.codeium/windsurf) @@ -130,6 +131,7 @@ describe("mirrorGlobalSkills", () => { }); const agents = mirrored.map((m) => m.agent); expect(agents).toContain("cursor"); + expect(agents).toContain("bob"); expect(agents).toContain("goose"); expect(agents).not.toContain("windsurf"); @@ -139,6 +141,12 @@ describe("mirrorGlobalSkills", () => { expect(realpathSync(link)).toBe(realpathSync(join(home, ".claude", "skills", "hyperframes"))); expect(existsSync(join(link, "SKILL.md"))).toBe(true); + const bobLink = join(home, ".bob", "skills", "hyperframes"); + expect(lstatSync(bobLink).isSymbolicLink()).toBe(true); + expect(realpathSync(bobLink)).toBe( + realpathSync(join(home, ".claude", "skills", "hyperframes")), + ); + // goose lands in the XDG config dir (~/.config/goose), not ~/.goose expect( existsSync(join(home, ".config", "goose", "skills", "hyperframes-core", "SKILL.md")), @@ -283,6 +291,7 @@ describe("AGENT_GLOBAL_DIRS (generated table)", () => { const byAgent = new Map(AGENT_GLOBAL_DIRS.map((e) => [e.agent, e])); expect(byAgent.get("claude-code")).toMatchObject({ base: "claudeHome", sub: "skills" }); expect(byAgent.get("cursor")).toMatchObject({ base: "home", sub: ".cursor/skills" }); + expect(byAgent.get("bob")).toMatchObject({ base: "home", sub: ".bob/skills" }); expect(byAgent.get("codex")).toMatchObject({ base: "codexHome", sub: "skills" }); expect(byAgent.get("goose")).toMatchObject({ base: "configHome", sub: "goose/skills" }); expect(byAgent.get("windsurf")).toMatchObject({