From 2de36d72a8bb19f4756f6d5394f373aeda158d7c Mon Sep 17 00:00:00 2001 From: Matthew Williams Date: Wed, 25 Mar 2026 07:25:19 -0400 Subject: [PATCH 1/2] Add Copilot CLI support via Claude-compatible skills Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 5 +++ README.md | 21 +++++++++++- setup | 65 ++++++++++++++++++++++++------------- test/gen-skill-docs.test.ts | 13 ++++++-- 4 files changed, 77 insertions(+), 27 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4c3157169..69c125526e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -232,6 +232,11 @@ To add a browse command, add it to `browse/src/commands.ts`. To add a snapshot f gstack generates SKILL.md files for two hosts: **Claude** (`.claude/skills/`) and **Codex** (`.agents/skills/`). Every template change needs to be generated for both. +**Copilot CLI support intentionally reuses the Claude-compatible `.claude/skills/` output.** +There is no separate `--host copilot` generator mode to maintain. If you change +install paths or setup behavior, make sure the Claude-format skill tree still works +for Copilot's documented `.claude/skills` support. + ### Generating for both hosts ```bash diff --git a/README.md b/README.md index fd81d78cee..7bd76a1d3a 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Fork it. Improve it. Make it yours. And if you want to hate on free open source ## Install — 30 seconds -**Requirements:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Git](https://git-scm.com/), [Bun](https://bun.sh/) v1.0+, [Node.js](https://nodejs.org/) (Windows only) +**Requirements:** One supported host ([Claude Code](https://docs.anthropic.com/en/docs/claude-code), [GitHub Copilot CLI](https://docs.github.com/copilot/how-tos/use-copilot-agents/use-copilot-cli), or another SKILL.md-compatible agent), [Git](https://git-scm.com/), [Bun](https://bun.sh/) v1.0+, [Node.js](https://nodejs.org/) (Windows only) ### Step 1: Install on your machine @@ -54,6 +54,23 @@ Open Claude Code and paste this. Claude does the rest. Real files get committed to your repo (not a submodule), so `git clone` just works. Everything lives inside `.claude/`. Nothing touches your PATH or runs in the background. +### GitHub Copilot CLI + +GitHub Copilot CLI officially supports skills from `.claude/skills` and +`~/.claude/skills`, so the lowest-friction gstack install reuses the same +Claude-compatible layout instead of introducing a separate Copilot-only tree. + +Install once for your user account: + +```bash +git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack +cd ~/.claude/skills/gstack && ./setup --host copilot +``` + +For repo-local installs, vendoring gstack into `.claude/skills/gstack` works with +Copilot CLI too. Put your guidance in `AGENTS.md` or `.github/copilot-instructions.md`, +then use `/skills reload` if Copilot is already running. + ### Codex, Gemini CLI, or Cursor gstack works on any agent that supports the [SKILL.md standard](https://github.com/anthropics/claude-code). Skills live in `.agents/skills/` and are discovered automatically. @@ -228,6 +245,8 @@ Data is stored in [Supabase](https://supabase.com) (open source Firebase alterna **Windows users:** gstack works on Windows 11 via Git Bash or WSL. Node.js is required in addition to Bun — Bun has a known bug with Playwright's pipe transport on Windows ([bun#4253](https://github.com/oven-sh/bun/issues/4253)). The browse server automatically falls back to Node.js. Make sure both `bun` and `node` are on your PATH. +**Copilot CLI doesn't see the skills?** Run `/skills list` or `/skills reload`. If you're using the Claude-compatible install path, rerun: `cd ~/.claude/skills/gstack && ./setup --host copilot` + **Claude says it can't see the skills?** Make sure your project's `CLAUDE.md` has a gstack section. Add this: ``` diff --git a/setup b/setup index bfae87851c..5d30326a12 100755 --- a/setup +++ b/setup @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# gstack setup — build browser binary + register skills with Claude Code / Codex +# gstack setup — build browser binary + register skills with Claude Code / Copilot CLI / Codex set -e if ! command -v bun >/dev/null 2>&1; then @@ -25,7 +25,7 @@ HOST="claude" LOCAL_INSTALL=0 while [ $# -gt 0 ]; do case "$1" in - --host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;; + --host) [ -z "$2" ] && echo "Missing value for --host (expected claude, copilot, codex, kiro, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;; --host=*) HOST="${1#--host=}"; shift ;; --local) LOCAL_INSTALL=1; shift ;; *) shift ;; @@ -33,36 +33,45 @@ while [ $# -gt 0 ]; do done case "$HOST" in - claude|codex|kiro|auto) ;; - *) echo "Unknown --host value: $HOST (expected claude, codex, kiro, or auto)" >&2; exit 1 ;; + claude|copilot|codex|kiro|auto) ;; + *) echo "Unknown --host value: $HOST (expected claude, copilot, codex, kiro, or auto)" >&2; exit 1 ;; esac # --local: install to .claude/skills/ in the current working directory +# Copilot CLI officially supports .claude/skills and ~/.claude/skills, so it +# shares the same lowest-friction local layout as Claude Code. if [ "$LOCAL_INSTALL" -eq 1 ]; then - if [ "$HOST" = "codex" ]; then - echo "Error: --local is only supported for Claude Code (not Codex)." >&2 + if [ "$HOST" = "codex" ] || [ "$HOST" = "kiro" ]; then + echo "Error: --local is only supported for Claude Code and Copilot CLI (not Codex or Kiro)." >&2 exit 1 fi INSTALL_SKILLS_DIR="$(pwd)/.claude/skills" mkdir -p "$INSTALL_SKILLS_DIR" - HOST="claude" + [ "$HOST" = "auto" ] && HOST="claude" INSTALL_CODEX=0 + INSTALL_KIRO=0 fi # For auto: detect which agents are installed INSTALL_CLAUDE=0 +INSTALL_COPILOT=0 INSTALL_CODEX=0 INSTALL_KIRO=0 if [ "$HOST" = "auto" ]; then command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1 command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1 command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1 + if [ "$INSTALL_CLAUDE" -eq 0 ]; then + command -v copilot >/dev/null 2>&1 && INSTALL_COPILOT=1 + fi # If none found, default to claude - if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ]; then + if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_COPILOT" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ]; then INSTALL_CLAUDE=1 fi elif [ "$HOST" = "claude" ]; then INSTALL_CLAUDE=1 +elif [ "$HOST" = "copilot" ]; then + INSTALL_COPILOT=1 elif [ "$HOST" = "codex" ]; then INSTALL_CODEX=1 elif [ "$HOST" = "kiro" ]; then @@ -221,6 +230,25 @@ link_claude_skill_dirs() { fi } +install_claude_like() { + local host_label="$1" + if [ "$SKILLS_BASENAME" = "skills" ]; then + link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" + if [ "$LOCAL_INSTALL" -eq 1 ]; then + echo "gstack ready (project-local $host_label)." + echo " skills: $INSTALL_SKILLS_DIR" + else + echo "gstack ready ($host_label)." + [ "$host_label" = "copilot" ] && echo " skills: $INSTALL_SKILLS_DIR" + fi + echo " browse: $BROWSE_BIN" + else + echo "gstack ready ($host_label)." + echo " browse: $BROWSE_BIN" + echo " (skipped skill symlinks — not inside .claude/skills/)" + fi +} + # ─── Helper: link generated Codex skills into a skills parent directory ── # Installs from .agents/skills/gstack-* (the generated Codex-format skills) # instead of source dirs (which have Claude paths). @@ -338,7 +366,7 @@ create_codex_runtime_root() { fi } -# 4. Install for Claude (default) +# 4. Install for Claude / Copilot CLI (same .claude/skills layout) SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")" SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")" CODEX_REPO_LOCAL=0 @@ -347,20 +375,11 @@ if [ "$SKILLS_BASENAME" = "skills" ] && [ "$SKILLS_PARENT_BASENAME" = ".agents" fi if [ "$INSTALL_CLAUDE" -eq 1 ]; then - if [ "$SKILLS_BASENAME" = "skills" ]; then - link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" - if [ "$LOCAL_INSTALL" -eq 1 ]; then - echo "gstack ready (project-local)." - echo " skills: $INSTALL_SKILLS_DIR" - else - echo "gstack ready (claude)." - fi - echo " browse: $BROWSE_BIN" - else - echo "gstack ready (claude)." - echo " browse: $BROWSE_BIN" - echo " (skipped skill symlinks — not inside .claude/skills/)" - fi + install_claude_like "claude" +fi + +if [ "$INSTALL_COPILOT" -eq 1 ]; then + install_claude_like "copilot" fi # 5. Install for Codex diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index d8a071a1f7..df41c91156 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1478,17 +1478,24 @@ describe('setup script validation', () => { expect(fnBody).toContain('ln -snf "gstack/$skill_name"'); }); - test('setup supports --host auto|claude|codex|kiro', () => { + test('setup supports --host auto|claude|copilot|codex|kiro', () => { expect(setupContent).toContain('--host'); - expect(setupContent).toContain('claude|codex|kiro|auto'); + expect(setupContent).toContain('claude|copilot|codex|kiro|auto'); }); - test('auto mode detects claude, codex, and kiro binaries', () => { + test('auto mode detects claude, copilot, codex, and kiro binaries', () => { expect(setupContent).toContain('command -v claude'); + expect(setupContent).toContain('command -v copilot'); expect(setupContent).toContain('command -v codex'); expect(setupContent).toContain('command -v kiro-cli'); }); + test('Copilot setup reuses the Claude-compatible .claude/skills layout', () => { + expect(setupContent).toContain('INSTALL_COPILOT='); + expect(setupContent).toContain('GitHub Copilot CLI officially supports .claude/skills'); + expect(setupContent).toContain('install_claude_like "copilot"'); + }); + // T1: Sidecar skip guard — prevents .agents/skills/gstack from being linked as a skill test('link_codex_skill_dirs skips the gstack sidecar directory', () => { const fnStart = setupContent.indexOf('link_codex_skill_dirs()'); From 2ae36b6049c5773907973f7285f88e368d098e32 Mon Sep 17 00:00:00 2001 From: Matthew Williams Date: Wed, 25 Mar 2026 08:14:54 -0400 Subject: [PATCH 2/2] Add Copilot to global retro discovery Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- bin/gstack-global-discover.ts | 118 +++++++++++++++++++++++++++++++--- test/global-discover.test.ts | 56 +++++++++++++++- 3 files changed, 164 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7bd76a1d3a..267e377455 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan- | `/canary` | **SRE** | Post-deploy monitoring loop. Watches for console errors, performance regressions, and page failures. | | `/benchmark` | **Performance Engineer** | Baseline page load times, Core Web Vitals, and resource sizes. Compare before/after on every PR. | | `/document-release` | **Technical Writer** | Update all project docs to match what you just shipped. Catches stale READMEs automatically. | -| `/retro` | **Eng Manager** | Team-aware weekly retro. Per-person breakdowns, shipping streaks, test health trends, growth opportunities. `/retro global` runs across all your projects and AI tools (Claude Code, Codex, Gemini). | +| `/retro` | **Eng Manager** | Team-aware weekly retro. Per-person breakdowns, shipping streaks, test health trends, growth opportunities. `/retro global` runs across all your projects and AI tools (Claude Code, Codex, Gemini, Copilot). | | `/browse` | **QA Engineer** | Real Chromium browser, real clicks, real screenshots. ~100ms per command. | | `/setup-browser-cookies` | **Session Manager** | Import cookies from your real browser (Chrome, Arc, Brave, Edge) into the headless session. Test authenticated pages. | | `/autoplan` | **Review Pipeline** | One command, fully reviewed plan. Runs CEO → design → eng review automatically with encoded decision principles. Surfaces only taste decisions for your approval. | diff --git a/bin/gstack-global-discover.ts b/bin/gstack-global-discover.ts index e6c64f561d..c9fdb01f18 100644 --- a/bin/gstack-global-discover.ts +++ b/bin/gstack-global-discover.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun /** - * gstack-global-discover — Discover AI coding sessions across Claude Code, Codex CLI, and Gemini CLI. + * gstack-global-discover — Discover AI coding sessions across Claude Code, Codex CLI, Gemini CLI, and GitHub Copilot CLI. * Resolves each session's working directory to a git repo, deduplicates by normalized remote URL, * and outputs structured JSON to stdout. * @@ -17,7 +17,7 @@ import { homedir } from "os"; // ── Types ────────────────────────────────────────────────────────────────── interface Session { - tool: "claude_code" | "codex" | "gemini"; + tool: "claude_code" | "codex" | "gemini" | "copilot"; cwd: string; } @@ -25,7 +25,7 @@ interface Repo { name: string; remote: string; paths: string[]; - sessions: { claude_code: number; codex: number; gemini: number }; + sessions: { claude_code: number; codex: number; gemini: number; copilot: number }; } interface DiscoveryResult { @@ -36,6 +36,7 @@ interface DiscoveryResult { claude_code: { total_sessions: number; repos: number }; codex: { total_sessions: number; repos: number }; gemini: { total_sessions: number; repos: number }; + copilot: { total_sessions: number; repos: number }; }; total_sessions: number; total_repos: number; @@ -290,6 +291,42 @@ function extractCwdFromJsonl(filePath: string): string | null { return null; } +function extractCopilotCwdFromWorkspace(filePath: string): string | null { + try { + const text = readFileSync(filePath, { encoding: "utf-8" }); + const match = text.match(/^cwd:\s*(.+)$/m); + if (!match) return null; + return match[1].trim().replace(/^['"]|['"]$/g, ""); + } catch { + return null; + } +} + +function extractCopilotCwdFromEvents(filePath: string): string | null { + try { + const fd = openSync(filePath, "r"); + const buf = Buffer.alloc(16384); + const bytesRead = readSync(fd, buf, 0, 16384, 0); + closeSync(fd); + const lines = buf.toString("utf-8", 0, bytesRead).split("\n").slice(0, 25); + for (const line of lines) { + if (!line.trim()) continue; + try { + const obj = JSON.parse(line); + const cwd = obj.data?.context?.cwd; + if (obj.type === "session.start" && typeof cwd === "string") { + return cwd; + } + } catch { + continue; + } + } + } catch { + // File read error + } + return null; +} + function scanCodex(since: Date): Session[] { const sessionsDir = join(homedir(), ".codex", "sessions"); if (!existsSync(sessionsDir)) return []; @@ -440,6 +477,59 @@ function scanGemini(since: Date): Session[] { return sessions; } +function scanCopilot(since: Date): Session[] { + const sessionsDir = join(homedir(), ".copilot", "session-state"); + if (!existsSync(sessionsDir)) return []; + + const sessions: Session[] = []; + + let dirs: string[]; + try { + dirs = readdirSync(sessionsDir); + } catch { + return []; + } + + for (const dirName of dirs) { + const dirPath = join(sessionsDir, dirName); + try { + const stat = statSync(dirPath); + if (!stat.isDirectory()) continue; + } catch { + continue; + } + + const workspacePath = join(dirPath, "workspace.yaml"); + const eventsPath = join(dirPath, "events.jsonl"); + + const hasRecentWorkspace = existsSync(workspacePath) && (() => { + try { + return statSync(workspacePath).mtime >= since; + } catch { + return false; + } + })(); + const hasRecentEvents = existsSync(eventsPath) && (() => { + try { + return statSync(eventsPath).mtime >= since; + } catch { + return false; + } + })(); + if (!hasRecentWorkspace && !hasRecentEvents) continue; + + let cwd = hasRecentWorkspace ? extractCopilotCwdFromWorkspace(workspacePath) : null; + if ((!cwd || !existsSync(cwd)) && hasRecentEvents) { + cwd = extractCopilotCwdFromEvents(eventsPath); + } + if (!cwd || !existsSync(cwd)) continue; + + sessions.push({ tool: "copilot", cwd }); + } + + return sessions; +} + // ── Deduplication ────────────────────────────────────────────────────────── async function resolveAndDeduplicate(sessions: Session[]): Promise { @@ -496,7 +586,7 @@ async function resolveAndDeduplicate(sessions: Session[]): Promise { } } - const sessionCounts = { claude_code: 0, codex: 0, gemini: 0 }; + const sessionCounts = { claude_code: 0, codex: 0, gemini: 0, copilot: 0 }; for (const s of data.sessions) { sessionCounts[s.tool]++; } @@ -512,8 +602,8 @@ async function resolveAndDeduplicate(sessions: Session[]): Promise { // Sort by total sessions descending repos.sort( (a, b) => - b.sessions.claude_code + b.sessions.codex + b.sessions.gemini - - (a.sessions.claude_code + a.sessions.codex + a.sessions.gemini) + b.sessions.claude_code + b.sessions.codex + b.sessions.gemini + b.sessions.copilot - + (a.sessions.claude_code + a.sessions.codex + a.sessions.gemini + a.sessions.copilot) ); return repos; @@ -530,12 +620,13 @@ async function main() { const ccSessions = scanClaudeCode(sinceDate); const codexSessions = scanCodex(sinceDate); const geminiSessions = scanGemini(sinceDate); + const copilotSessions = scanCopilot(sinceDate); - const allSessions = [...ccSessions, ...codexSessions, ...geminiSessions]; + const allSessions = [...ccSessions, ...codexSessions, ...geminiSessions, ...copilotSessions]; // Summary to stderr console.error( - `Discovered: ${ccSessions.length} CC sessions, ${codexSessions.length} Codex sessions, ${geminiSessions.length} Gemini sessions` + `Discovered: ${ccSessions.length} CC sessions, ${codexSessions.length} Codex sessions, ${geminiSessions.length} Gemini sessions, ${copilotSessions.length} Copilot sessions` ); // Deduplicate @@ -547,6 +638,7 @@ async function main() { const ccRepos = new Set(repos.filter((r) => r.sessions.claude_code > 0).map((r) => r.remote)).size; const codexRepos = new Set(repos.filter((r) => r.sessions.codex > 0).map((r) => r.remote)).size; const geminiRepos = new Set(repos.filter((r) => r.sessions.gemini > 0).map((r) => r.remote)).size; + const copilotRepos = new Set(repos.filter((r) => r.sessions.copilot > 0).map((r) => r.remote)).size; const result: DiscoveryResult = { window: since, @@ -556,6 +648,7 @@ async function main() { claude_code: { total_sessions: ccSessions.length, repos: ccRepos }, codex: { total_sessions: codexSessions.length, repos: codexRepos }, gemini: { total_sessions: geminiSessions.length, repos: geminiRepos }, + copilot: { total_sessions: copilotSessions.length, repos: copilotRepos }, }, total_sessions: allSessions.length, total_repos: repos.length, @@ -566,15 +659,20 @@ async function main() { } else { // Summary format console.log(`Window: ${since} (since ${startDate})`); - console.log(`Sessions: ${allSessions.length} total (CC: ${ccSessions.length}, Codex: ${codexSessions.length}, Gemini: ${geminiSessions.length})`); + console.log(`Sessions: ${allSessions.length} total (CC: ${ccSessions.length}, Codex: ${codexSessions.length}, Gemini: ${geminiSessions.length}, Copilot: ${copilotSessions.length})`); console.log(`Repos: ${repos.length} unique`); console.log(""); for (const repo of repos) { - const total = repo.sessions.claude_code + repo.sessions.codex + repo.sessions.gemini; + const total = + repo.sessions.claude_code + + repo.sessions.codex + + repo.sessions.gemini + + repo.sessions.copilot; const tools = []; if (repo.sessions.claude_code > 0) tools.push(`CC:${repo.sessions.claude_code}`); if (repo.sessions.codex > 0) tools.push(`Codex:${repo.sessions.codex}`); if (repo.sessions.gemini > 0) tools.push(`Gemini:${repo.sessions.gemini}`); + if (repo.sessions.copilot > 0) tools.push(`Copilot:${repo.sessions.copilot}`); console.log(` ${repo.name} (${total} sessions) — ${tools.join(", ")}`); console.log(` Remote: ${repo.remote}`); console.log(` Paths: ${repo.paths.join(", ")}`); diff --git a/test/global-discover.test.ts b/test/global-discover.test.ts index c8d489f4a3..e7424a9c7f 100644 --- a/test/global-discover.test.ts +++ b/test/global-discover.test.ts @@ -129,6 +129,58 @@ describe("gstack-global-discover", () => { const json = JSON.parse(result.stdout); expect(json.total_sessions).toBeGreaterThanOrEqual(0); }); + + test("discovers Copilot sessions from workspace.yaml", () => { + const tempDir = mkdtempSync(join(tmpdir(), "gstack-global-discover-")); + try { + const homeDir = join(tempDir, "home"); + const repoDir = join(homeDir, "repo"); + mkdirSync(repoDir, { recursive: true }); + + const gitInit = spawnSync("git", ["init"], { + cwd: repoDir, + encoding: "utf-8", + timeout: 10000, + }); + expect(gitInit.status).toBe(0); + + const sessionDir = join( + homeDir, + ".copilot", + "session-state", + "123e4567-e89b-12d3-a456-426614174000" + ); + mkdirSync(sessionDir, { recursive: true }); + writeFileSync( + join(sessionDir, "workspace.yaml"), + `id: 123e4567-e89b-12d3-a456-426614174000\ncwd: ${repoDir}\ngit_root: ${repoDir}\nrepository: local/test\nbranch: main\n` + ); + + const result = spawnSync( + "bun", + ["run", scriptPath, "--since", "7d", "--format", "json"], + { + cwd: tempDir, + env: { ...process.env, HOME: homeDir }, + encoding: "utf-8", + timeout: 30000, + } + ); + + expect(result.status).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.total_sessions).toBe(1); + expect(json.total_repos).toBe(1); + expect(json.tools.copilot.total_sessions).toBe(1); + expect(json.tools.copilot.repos).toBe(1); + expect(json.repos[0].paths).toContain(repoDir); + expect(json.repos[0].sessions.copilot).toBe(1); + } finally { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }); + } + } + }); }); describe("discovery output structure", () => { @@ -151,6 +203,7 @@ describe("gstack-global-discover", () => { expect(repo.sessions).toHaveProperty("claude_code"); expect(repo.sessions).toHaveProperty("codex"); expect(repo.sessions).toHaveProperty("gemini"); + expect(repo.sessions).toHaveProperty("copilot"); } }); @@ -166,7 +219,8 @@ describe("gstack-global-discover", () => { const toolTotal = json.tools.claude_code.total_sessions + json.tools.codex.total_sessions + - json.tools.gemini.total_sessions; + json.tools.gemini.total_sessions + + json.tools.copilot.total_sessions; expect(json.total_sessions).toBe(toolTotal); });