mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
v1.92.20.0 /retro global counts Kimi Code, OMP and Antigravity sessions (#3740)
* global-discover: scanner registry; Kimi Code, OMP and Antigravity sessions (#2917, #1977) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: /retro global lists Kimi Code, OMP and Antigravity; TODOS marks #2917 shipped Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
dcc1c51bfc
commit
fd5fcb4318
10 files changed
+305
-68
No files matched your search
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [1.92.20.0] - 2026-11-01
|
||||
|
||||
**`/retro global` now sees Kimi Code, OMP and Antigravity sessions (#2917, #1977).** `gstack-global-discover` scanned only Claude Code, Codex and Gemini. A week spent in other agent CLIs went uncounted, and nothing showed a zero to notice.
|
||||
|
||||
### Added
|
||||
- **Kimi Code** (`~/.kimi-code/session_index.jsonl`, window by `state.json` updatedAt, repo by workDir) and **OMP** (`~/.omp/agent/sessions/*/*.jsonl`, window by file mtime, repo by the cwd in the session file; `~/.omp/logs` is never counted) are counted per repo.
|
||||
- **Antigravity** (`~/.gemini/antigravity-cli/conversations/*.db`) is counted by the later of each database's and its `-wal` file's mtime. Its sessions count in `total_sessions` but belong to no repo, because the CLI stores the working directory inside an undocumented blob.
|
||||
|
||||
### Changed
|
||||
- **One scanner registry** drives the JSON, the summary and the stderr line. Every registered tool appears in `tools`, zeros included, with a `label`. Antigravity's entry carries `unattributed: true`. Existing keys (`claude_code`, `codex`, `gemini`) keep their paths, and each repo's `sessions` lists every attributed tool.
|
||||
- **/retro global** builds its session counts and Tool Usage Analysis from that list. A tool missing from an older saved retro counts as 0.
|
||||
|
||||
Thanks to @richardiitse (#2917) and @Akagilnc (#1977) for the reports and the store layouts.
|
||||
|
||||
## [1.92.19.0] - 2026-11-01
|
||||
|
||||
**On Linux and Windows the renderer goes straight to gstack's own browser instead of asking for Aside (#2864).** `probeAside()` in `lib/aside-render.ts`, used by /make-pdf, /diagram and `gstack-render`, used to run `aside --version` on any platform. When that failed it told the user to install a browser that only ships for macOS. Off macOS it now answers `NEEDS_ASIDE` with "Aside is macOS-only (this is linux)", without starting a process, and the renderer moves on to gstack's own browser. macOS behavior and `GSTACK_SKIP_ASIDE=1` are unchanged.
|
||||
|
||||
@@ -167,7 +167,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan-
|
||||
| `/eval-plan` | **Eval Planner** | Preregister a paid eval before the first priced call: bars, arms, decoys and stop rules, a $0 stub-model dry run over the real inputs, and a three-item priced pilot whose extrapolation and spend table gate the approval. |
|
||||
| `/document-release` | **Technical Writer** | Audit changed behavior against project docs on every ship, before final verification and publication. Also runs standalone. Shows updated, reviewed/current or blocked docs and any remaining gaps. |
|
||||
| `/document-generate` | **Documentation Author** | Generate missing docs from scratch using the Diataxis framework. Researches the codebase first, then writes reference / how-to / tutorial / explanation docs that actually match the code. Invokable standalone or chained from `/document-release` when the coverage map finds gaps. Learn more: [tutorial](docs/tutorial-document-generate.md) • [how-to](docs/howto-document-a-shipped-feature.md) • [why Diataxis](docs/explanation-diataxis-in-gstack.md). |
|
||||
| `/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, Kimi Code, OMP, Antigravity). |
|
||||
| `/browse` | **QA Engineer** | Give the agent eyes. Drives your [Aside](https://aside.com) browser first — your real sessions, real clicks, real screenshots — through deterministic `aside repl` scripts. No Aside? It falls back to gstack's own Chromium: real clicks, ~100ms per command, and `/open-gstack-browser` shows it headed with sidebar, anti-bot stealth, and auto model routing. Every other browser skill stands on it. |
|
||||
| `/scrape` | **Data Extractor** | Pull structured data off a web page — tables, lists, prices — in your Aside browser with the page's real logged-in state. On the fallback browser, `/skillify` turns the flow into a permanent browser-skill that runs in ~200ms next time. |
|
||||
| `/setup-browser-cookies` | **Session Manager** | Copy selected cookies from Chrome, Chromium, Brave, Edge, Windows-only Opera and Opera GX, or macOS-only Comet and Arc into gstack's bundled browser. Choose your profile and domains; copying and sign-in verification are separate. Only needed on the fallback path — Aside already has your sessions. |
|
||||
|
||||
@@ -82,7 +82,7 @@ Each item was decided in the Wave 9 plan and parked with its reason. Credit the
|
||||
|
||||
The 73 open feature requests (75 with #3061 and #3122, which the themes below also name), grouped by theme with counted demand (issues in the theme). Each issue stays open under the `parked` label with a link to its theme here (the rule is in docs/triage.md); the three largest cross-cutting themes (new hosts, provider pluralism, install footprint) also have tracking issues. Pick a theme up as a whole and credit every linked reporter.
|
||||
|
||||
- **New hosts (8 issues, plus #3061 above and 9 PRs parked on Oct 8)** — Kimi CLI (#1072), Gemini CLI (#1247), Pi Agent (#1288), Claude Desktop Cowork (#1345), Letta Code (#2117), team mode for non-Claude hosts (#890), `/retro global` discovery of Antigravity (#1977) and Kimi Code / OMP sessions (#2917). Waits for the host-config work; see "Parked contributor ideas (Oct 8 triage)" for the parked host PRs. **Priority:** P3.
|
||||
- **New hosts (7 issues, plus #3061 above and 9 PRs parked on Oct 8)** — Kimi CLI (#1072), Gemini CLI (#1247), Pi Agent (#1288), Claude Desktop Cowork (#1345), Letta Code (#2117), team mode for non-Claude hosts (#890), `/retro global` repo attribution for Antigravity sessions (#1977: they are counted now, but the working directory sits in an undocumented blob). Kimi Code and OMP discovery (#2917) shipped with the scanner registry in `bin/gstack-global-discover.ts`. Waits for the host-config work; see "Parked contributor ideas (Oct 8 triage)" for the parked host PRs. **Priority:** P3.
|
||||
- **Provider pluralism (7 issues, plus #3122)** — Gemini CLI as an outside-voice backend (#619), more image providers in `design` beyond OpenAI and Gemini (Gemini shipped for #990), Codex-native `image_gen` in the `design` binary's own generate, evolve and prototype calls (#2174; design-shotgun itself already uses `$imagegen` on Codex since #2175), GPT-6 Sol/Luna tiers in Codex call routing (#2943), a `/grok` review port (#2947), Codex review token usage (#2948), Codex refresh-token recovery docs (#1542), and a configurable PR review bot instead of hard-coded Greptile in /review, /ship and /retro (#3122, folded here by decision D4). One provider seam serves all of them. **Priority:** P3.
|
||||
- **Install footprint and layout (7 issues)** — skills.sh distribution (#113), `~/.gstack` as the global install path (#753), a repo-local install that keeps state in the project (#1235), five compiled binaries each bundling Bun (#1245), Windows junctions instead of copies (#1877), the 1.1 GB `~/.claude/skills` footprint (#2199), and a skill-only mode (#2528). Decision D6: setup keeps the staged copy on Windows, because it works today and links change what every cleanup deletes; #1943 (@JonasFocus, junctions) and #2129 (@TTmo123, symlinks with the privilege check) are closed with credit and their designs stay here as the starting point. See also "install footprint follow-ups from the October fix wave". **Priority:** P3.
|
||||
- **New skills (12 issues)** — GUI kanban for plan reviews (#247), journey maps (#729), UI-doc review (#851), assumption verification `/avl` (#1058), `/a11y-check` (#1104), `/env-check` (#1106), native plan writing (#1170), `/plan-experiment` and `/review-experiment` (#1195), `/plan-status` (#1343), language-specific reviewers (#1753), `/benchmark-mobile` (#1754), session start/end skills (#2088). **Priority:** P3.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.92.19.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.92.20.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+167
-42
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, statSync, readFileSync, openSync, readSync, closeSync } from "fs";
|
||||
import { join, basename, dirname, resolve } from "path";
|
||||
import { join, basename, dirname, resolve, isAbsolute } from "path";
|
||||
import { spawnSync } from "child_process";
|
||||
import { homedir } from "os";
|
||||
import { canonicalRemote } from "../lib/remote-identity";
|
||||
@@ -20,8 +20,11 @@ import { canonicalRemote } from "../lib/remote-identity";
|
||||
type CodexOriginator = "cli" | "desktop" | "exec" | "claude_code" | "other";
|
||||
type CodexOriginatorCounts = Record<CodexOriginator, number>;
|
||||
|
||||
type ToolId = "claude_code" | "codex" | "gemini" | "kimi_code" | "omp";
|
||||
type ToolCounts = Record<ToolId, number>;
|
||||
|
||||
interface Session {
|
||||
tool: "claude_code" | "codex" | "gemini";
|
||||
tool: ToolId;
|
||||
cwd: string;
|
||||
codexOriginator?: CodexOriginator;
|
||||
}
|
||||
@@ -30,19 +33,24 @@ interface Repo {
|
||||
name: string;
|
||||
remote: string;
|
||||
paths: string[];
|
||||
sessions: { claude_code: number; codex: number; gemini: number };
|
||||
sessions: ToolCounts;
|
||||
codex_originators: CodexOriginatorCounts;
|
||||
}
|
||||
|
||||
interface ToolSummary {
|
||||
label: string;
|
||||
total_sessions: number;
|
||||
repos: number;
|
||||
originators?: CodexOriginatorCounts;
|
||||
/** Sessions counted without a recorded cwd: they count in total_sessions, like sessions outside any repo, but belong to no repo. */
|
||||
unattributed?: true;
|
||||
}
|
||||
|
||||
interface DiscoveryResult {
|
||||
window: string;
|
||||
start_date: string;
|
||||
repos: Repo[];
|
||||
tools: {
|
||||
claude_code: { total_sessions: number; repos: number };
|
||||
codex: { total_sessions: number; repos: number; originators: CodexOriginatorCounts };
|
||||
gemini: { total_sessions: number; repos: number };
|
||||
};
|
||||
tools: Record<ToolId | "agy", ToolSummary>;
|
||||
total_sessions: number;
|
||||
total_repos: number;
|
||||
}
|
||||
@@ -521,6 +529,128 @@ function scanGemini(since: Date): Session[] {
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kimi Code: ~/.kimi-code/session_index.jsonl lists { sessionId, sessionDir, workDir };
|
||||
* the session's state.json carries updatedAt (epoch ms). workDir is the absolute cwd.
|
||||
*/
|
||||
function scanKimiCode(since: Date): Session[] {
|
||||
const root = join(homedir(), ".kimi-code");
|
||||
let index: string;
|
||||
try {
|
||||
index = readFileSync(join(root, "session_index.jsonl"), { encoding: "utf-8" });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const sessions: Session[] = [];
|
||||
for (const line of index.split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
let entry: any;
|
||||
try {
|
||||
entry = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (typeof entry?.sessionDir !== "string" || !entry.sessionDir) continue;
|
||||
const dir = isAbsolute(entry.sessionDir) ? entry.sessionDir : join(root, entry.sessionDir);
|
||||
let state: any = {};
|
||||
try {
|
||||
state = JSON.parse(readFileSync(join(dir, "state.json"), { encoding: "utf-8" }));
|
||||
} catch {}
|
||||
let updated = typeof state?.updatedAt === "number" ? state.updatedAt : NaN;
|
||||
if (Number.isNaN(updated)) {
|
||||
try {
|
||||
updated = statSync(dir).mtimeMs;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (updated < since.getTime()) continue;
|
||||
const cwd = typeof entry.workDir === "string" && entry.workDir ? entry.workDir : typeof state?.cwd === "string" ? state.cwd : null;
|
||||
if (cwd) sessions.push({ tool: "kimi_code", cwd });
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* OMP: one <timestamp>_<uuid>.jsonl per session under ~/.omp/agent/sessions/<workspace>/.
|
||||
* The workspace directory name is a lossy encoding of the path, so the cwd comes from
|
||||
* the session file itself. A session counts when its file was written inside the
|
||||
* window, like the other scanners; ~/.omp/logs holds per-launch logs and is never read.
|
||||
*/
|
||||
function scanOmp(since: Date, stats: CwdScanStats): Session[] {
|
||||
const sessionsDir = join(homedir(), ".omp", "agent", "sessions");
|
||||
let workspaces: string[];
|
||||
try {
|
||||
workspaces = readdirSync(sessionsDir);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const sessions: Session[] = [];
|
||||
for (const workspace of workspaces) {
|
||||
const dir = join(sessionsDir, workspace);
|
||||
let files: string[];
|
||||
try {
|
||||
files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const file of files) {
|
||||
const path = join(dir, file);
|
||||
try {
|
||||
if (statSync(path).mtime < since) continue;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const cwd = extractCwdFromJsonl(path, stats);
|
||||
if (cwd) sessions.push({ tool: "omp", cwd });
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Antigravity CLI (agy) keeps each conversation as SQLite under
|
||||
* ~/.gemini/antigravity-cli/conversations/*.db, with the cwd inside an undocumented
|
||||
* blob (#1977). Sessions are counted by the later of the .db and its -wal file's
|
||||
* mtime and are not attributed to a repo.
|
||||
*/
|
||||
function countAntigravity(since: Date): number {
|
||||
const dir = join(homedir(), ".gemini", "antigravity-cli", "conversations");
|
||||
let files: string[];
|
||||
try {
|
||||
files = readdirSync(dir).filter((f) => f.endsWith(".db"));
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
let count = 0;
|
||||
for (const file of files) {
|
||||
let latest = 0;
|
||||
for (const path of [join(dir, file), join(dir, `${file}-wal`)]) {
|
||||
try {
|
||||
latest = Math.max(latest, statSync(path).mtimeMs);
|
||||
} catch {}
|
||||
}
|
||||
if (latest >= since.getTime()) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const SCANNERS: ReadonlyArray<{ id: ToolId; label: string; scan: (since: Date, stats: CwdScanStats) => Session[] }> = [
|
||||
{ id: "claude_code", label: "CC", scan: scanClaudeCode },
|
||||
{ id: "codex", label: "Codex", scan: (since) => scanCodex(since) },
|
||||
{ id: "gemini", label: "Gemini", scan: (since) => scanGemini(since) },
|
||||
{ id: "kimi_code", label: "Kimi Code", scan: (since) => scanKimiCode(since) },
|
||||
{ id: "omp", label: "OMP", scan: scanOmp },
|
||||
];
|
||||
|
||||
function emptyCounts(): ToolCounts {
|
||||
return Object.fromEntries(SCANNERS.map((s) => [s.id, 0])) as ToolCounts;
|
||||
}
|
||||
|
||||
function countTotal(counts: ToolCounts): number {
|
||||
return SCANNERS.reduce((n, s) => n + counts[s.id], 0);
|
||||
}
|
||||
|
||||
// ── Deduplication ──────────────────────────────────────────────────────────
|
||||
|
||||
async function resolveAndDeduplicate(sessions: Session[]): Promise<Repo[]> {
|
||||
@@ -555,7 +685,7 @@ async function resolveAndDeduplicate(sessions: Session[]): Promise<Repo[]> {
|
||||
}
|
||||
}
|
||||
|
||||
const sessionCounts = { claude_code: 0, codex: 0, gemini: 0 };
|
||||
const sessionCounts = emptyCounts();
|
||||
for (const s of data.sessions) {
|
||||
sessionCounts[s.tool]++;
|
||||
}
|
||||
@@ -570,11 +700,7 @@ async function resolveAndDeduplicate(sessions: Session[]): Promise<Repo[]> {
|
||||
}
|
||||
|
||||
// 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)
|
||||
);
|
||||
repos.sort((a, b) => countTotal(b.sessions) - countTotal(a.sessions));
|
||||
|
||||
return repos;
|
||||
}
|
||||
@@ -588,21 +714,19 @@ async function main() {
|
||||
|
||||
// Run all scanners
|
||||
const cwdStats: CwdScanStats = { oversizedLines: 0 };
|
||||
const ccSessions = scanClaudeCode(sinceDate, cwdStats);
|
||||
const codexSessions = scanCodex(sinceDate);
|
||||
const geminiSessions = scanGemini(sinceDate);
|
||||
|
||||
const allSessions = [...ccSessions, ...codexSessions, ...geminiSessions];
|
||||
const byTool = SCANNERS.map((s) => ({ ...s, sessions: s.scan(sinceDate, cwdStats) }));
|
||||
const agySessions = countAntigravity(sinceDate);
|
||||
const allSessions = byTool.flatMap((t) => t.sessions);
|
||||
const codexSessions = byTool.find((t) => t.id === "codex")!.sessions;
|
||||
const toolCounts = byTool.map((t) => `${t.label}: ${t.sessions.length}`).join(", ");
|
||||
|
||||
// Summary to stderr
|
||||
console.error(
|
||||
`Discovered: ${ccSessions.length} CC sessions, ${codexSessions.length} Codex sessions, ${geminiSessions.length} Gemini sessions`
|
||||
);
|
||||
console.error(`Discovered: ${toolCounts}; Antigravity: ${agySessions} (repo unattributed)`);
|
||||
|
||||
if (cwdStats.oversizedLines > 0) {
|
||||
console.error(
|
||||
`Skipped ${cwdStats.oversizedLines} Claude Code JSONL line(s) over 1 MiB while reading session cwds; ` +
|
||||
`a session whose cwd appears only after such lines counts with its project directory's other sessions.`,
|
||||
`Skipped ${cwdStats.oversizedLines} session JSONL line(s) over 1 MiB while reading session cwds; ` +
|
||||
`a Claude Code session whose cwd appears only after such lines counts with its project directory's other sessions.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -611,23 +735,25 @@ async function main() {
|
||||
|
||||
console.error(`→ ${repos.length} unique repos`);
|
||||
|
||||
// Count per-tool repo counts
|
||||
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 codexOriginators = countCodexOriginators(codexSessions);
|
||||
const tools = Object.fromEntries(
|
||||
byTool.map((t) => [
|
||||
t.id,
|
||||
{
|
||||
label: t.label,
|
||||
total_sessions: t.sessions.length,
|
||||
repos: repos.filter((r) => r.sessions[t.id] > 0).length,
|
||||
...(t.id === "codex" ? { originators: codexOriginators } : {}),
|
||||
},
|
||||
]),
|
||||
) as Record<ToolId, ToolSummary>;
|
||||
|
||||
const result: DiscoveryResult = {
|
||||
window: since,
|
||||
start_date: startDate,
|
||||
repos,
|
||||
tools: {
|
||||
claude_code: { total_sessions: ccSessions.length, repos: ccRepos },
|
||||
codex: { total_sessions: codexSessions.length, repos: codexRepos, originators: codexOriginators },
|
||||
gemini: { total_sessions: geminiSessions.length, repos: geminiRepos },
|
||||
},
|
||||
total_sessions: allSessions.length,
|
||||
tools: { ...tools, agy: { label: "Antigravity", total_sessions: agySessions, repos: 0, unattributed: true } },
|
||||
total_sessions: allSessions.length + agySessions,
|
||||
total_repos: repos.length,
|
||||
};
|
||||
|
||||
@@ -636,17 +762,16 @@ 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 + agySessions} total (${toolCounts}, Antigravity: ${agySessions})`);
|
||||
if (codexSessions.length > 0) console.log(` Codex originators: ${formatCodexOriginators(codexOriginators)}`);
|
||||
if (agySessions > 0) console.log(` Antigravity sessions are not attributed to a repo`);
|
||||
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 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} (${formatCodexOriginators(repo.codex_originators)})`);
|
||||
if (repo.sessions.gemini > 0) tools.push(`Gemini:${repo.sessions.gemini}`);
|
||||
console.log(` ${repo.name} (${total} sessions) — ${tools.join(", ")}`);
|
||||
const parts = byTool
|
||||
.filter((t) => repo.sessions[t.id] > 0)
|
||||
.map((t) => (t.id === "codex" ? `Codex:${repo.sessions.codex} (${formatCodexOriginators(repo.codex_originators)})` : `${t.label}:${repo.sessions[t.id]}`));
|
||||
console.log(` ${repo.name} (${countTotal(repo.sessions)} sessions) — ${parts.join(", ")}`);
|
||||
console.log(` Remote: ${repo.remote}`);
|
||||
console.log(` Paths: ${repo.paths.join(", ")}`);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.92.19",
|
||||
"version": "1.92.20",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+7
-8
@@ -728,7 +728,7 @@ From the commit timestamps gathered in Step 3, group by date. For each date, cou
|
||||
|
||||
From the discovery JSON, analyze tool usage patterns:
|
||||
- Which AI tool is used for which repos (exclusive vs. shared)
|
||||
- Session count per tool
|
||||
- Session count per tool (each discovery `tools` entry, by `label`)
|
||||
- Behavioral patterns (e.g., "Codex used exclusively for myapp, Claude Code for everything else")
|
||||
|
||||
### Global Step 7: Aggregate and draft narrative
|
||||
@@ -757,7 +757,7 @@ pad names to the longest name and never truncate them.
|
||||
║
|
||||
║ [N] commits across [M] projects
|
||||
║ +[X]k LOC added · [Y]k LOC deleted · [Z]k net
|
||||
║ [N] AI coding sessions (CC: X, Codex: Y, Gemini: Z)
|
||||
║ [N] AI coding sessions ([label]: X per tool)
|
||||
║ [N]-day shipping streak 🔥
|
||||
║
|
||||
║ PROJECTS
|
||||
@@ -804,7 +804,7 @@ Full team/project analysis follows the personal card.
|
||||
| Projects active | N |
|
||||
| Total commits (all repos, all contributors) | N |
|
||||
| Total LOC | +N / -N |
|
||||
| AI coding sessions | N (CC: X, Codex: Y, Gemini: Z) |
|
||||
| AI coding sessions | N ([label]: X per tool) |
|
||||
| Active days | N |
|
||||
| Global shipping streak (any contributor, any repo) | N consecutive days |
|
||||
| Context switches/day | N avg (max: M) |
|
||||
@@ -844,9 +844,8 @@ Format:
|
||||
|
||||
### Tool Usage Analysis
|
||||
Per-tool breakdown with behavioral patterns:
|
||||
- Claude Code: N sessions across M repos — patterns observed
|
||||
- Codex: N sessions across M repos — patterns observed
|
||||
- Gemini: N sessions across M repos — patterns observed
|
||||
- [label]: N sessions across M repos — patterns observed
|
||||
- `unattributed` (Antigravity): sessions, no repos
|
||||
|
||||
### Ship of the Week (Global)
|
||||
Highest-impact PR across ALL projects. Identify by LOC and commit messages.
|
||||
@@ -869,7 +868,7 @@ ls -t "$GSTACK_STATE_ROOT"/retros/global-*.json 2>/dev/null | head -5
|
||||
|
||||
**Only compare against a prior retro with the same `window` value** (e.g., 7d vs 7d). If the most recent prior retro has a different window, skip comparison and note: "Prior global retro used a different window — skipping comparison."
|
||||
|
||||
If a matching prior retro exists, load it with the Read tool. Show a **Trends vs Last Global Retro** table with deltas for key metrics: total commits, LOC, sessions, streak, context switches/day.
|
||||
If a matching prior retro exists, load it with the Read tool. Show a **Trends vs Last Global Retro** table with deltas for key metrics: total commits, LOC, sessions, streak, context switches/day. Missing tools count as 0.
|
||||
|
||||
If no prior global retros exist, append: "First global retro recorded — run again next week to see trends."
|
||||
|
||||
@@ -913,7 +912,7 @@ Use the Write tool to save JSON to the printed `RETRO_FILE`:
|
||||
"deletions": 4200,
|
||||
"projects": 5,
|
||||
"active_days": 6,
|
||||
"sessions": { "claude_code": 48, "codex": 8, "gemini": 3 },
|
||||
"sessions": { "claude_code": 48, "codex": 8, "gemini": 3, "omp": 1 },
|
||||
"global_streak_days": 52,
|
||||
"avg_context_switches_per_day": 2.1
|
||||
},
|
||||
|
||||
+7
-8
@@ -549,7 +549,7 @@ From the commit timestamps gathered in Step 3, group by date. For each date, cou
|
||||
|
||||
From the discovery JSON, analyze tool usage patterns:
|
||||
- Which AI tool is used for which repos (exclusive vs. shared)
|
||||
- Session count per tool
|
||||
- Session count per tool (each discovery `tools` entry, by `label`)
|
||||
- Behavioral patterns (e.g., "Codex used exclusively for myapp, Claude Code for everything else")
|
||||
|
||||
### Global Step 7: Aggregate and draft narrative
|
||||
@@ -578,7 +578,7 @@ pad names to the longest name and never truncate them.
|
||||
║
|
||||
║ [N] commits across [M] projects
|
||||
║ +[X]k LOC added · [Y]k LOC deleted · [Z]k net
|
||||
║ [N] AI coding sessions (CC: X, Codex: Y, Gemini: Z)
|
||||
║ [N] AI coding sessions ([label]: X per tool)
|
||||
║ [N]-day shipping streak 🔥
|
||||
║
|
||||
║ PROJECTS
|
||||
@@ -625,7 +625,7 @@ Full team/project analysis follows the personal card.
|
||||
| Projects active | N |
|
||||
| Total commits (all repos, all contributors) | N |
|
||||
| Total LOC | +N / -N |
|
||||
| AI coding sessions | N (CC: X, Codex: Y, Gemini: Z) |
|
||||
| AI coding sessions | N ([label]: X per tool) |
|
||||
| Active days | N |
|
||||
| Global shipping streak (any contributor, any repo) | N consecutive days |
|
||||
| Context switches/day | N avg (max: M) |
|
||||
@@ -665,9 +665,8 @@ Format:
|
||||
|
||||
### Tool Usage Analysis
|
||||
Per-tool breakdown with behavioral patterns:
|
||||
- Claude Code: N sessions across M repos — patterns observed
|
||||
- Codex: N sessions across M repos — patterns observed
|
||||
- Gemini: N sessions across M repos — patterns observed
|
||||
- [label]: N sessions across M repos — patterns observed
|
||||
- `unattributed` (Antigravity): sessions, no repos
|
||||
|
||||
### Ship of the Week (Global)
|
||||
Highest-impact PR across ALL projects. Identify by LOC and commit messages.
|
||||
@@ -690,7 +689,7 @@ ls -t "$GSTACK_STATE_ROOT"/retros/global-*.json 2>/dev/null | head -5
|
||||
|
||||
**Only compare against a prior retro with the same `window` value** (e.g., 7d vs 7d). If the most recent prior retro has a different window, skip comparison and note: "Prior global retro used a different window — skipping comparison."
|
||||
|
||||
If a matching prior retro exists, load it with the Read tool. Show a **Trends vs Last Global Retro** table with deltas for key metrics: total commits, LOC, sessions, streak, context switches/day.
|
||||
If a matching prior retro exists, load it with the Read tool. Show a **Trends vs Last Global Retro** table with deltas for key metrics: total commits, LOC, sessions, streak, context switches/day. Missing tools count as 0.
|
||||
|
||||
If no prior global retros exist, append: "First global retro recorded — run again next week to see trends."
|
||||
|
||||
@@ -734,7 +733,7 @@ Use the Write tool to save JSON to the printed `RETRO_FILE`:
|
||||
"deletions": 4200,
|
||||
"projects": 5,
|
||||
"active_days": 6,
|
||||
"sessions": { "claude_code": 48, "codex": 8, "gemini": 3 },
|
||||
"sessions": { "claude_code": 48, "codex": 8, "gemini": 3, "omp": 1 },
|
||||
"global_streak_days": 52,
|
||||
"avg_context_switches_per_day": 2.1
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from "fs";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, utimesSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
import { spawnSync } from "child_process";
|
||||
@@ -417,11 +417,9 @@ describe("gstack-global-discover", () => {
|
||||
const json = JSON.parse(result.stdout);
|
||||
|
||||
// Total sessions should equal sum across tools
|
||||
const toolTotal =
|
||||
json.tools.claude_code.total_sessions +
|
||||
json.tools.codex.total_sessions +
|
||||
json.tools.gemini.total_sessions;
|
||||
const toolTotal = Object.values(json.tools as Record<string, { total_sessions: number }>).reduce((n, t) => n + t.total_sessions, 0);
|
||||
expect(json.total_sessions).toBe(toolTotal);
|
||||
for (const key of ["claude_code", "codex", "gemini", "kimi_code", "omp", "agy"]) expect(json.tools).toHaveProperty(key);
|
||||
});
|
||||
|
||||
test("deduplicates Conductor workspaces by remote", () => {
|
||||
@@ -439,6 +437,108 @@ describe("gstack-global-discover", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kimi Code, OMP and Antigravity sessions (#2917, #1977)", () => {
|
||||
let home: string;
|
||||
beforeEach(() => {
|
||||
home = mkdtempSync(join(tmpdir(), "gstack-more-tools-"));
|
||||
});
|
||||
afterEach(() => rmSync(home, { recursive: true, force: true }));
|
||||
|
||||
const gitEnv = () => ({ ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: join(home, ".gitconfig"), GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@example.com", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@example.com" });
|
||||
function repo(rel: string, remote: string): string {
|
||||
const dir = join(home, rel);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
for (const args of [["init", "-q"], ["commit", "-q", "--allow-empty", "-m", "init"], ["remote", "add", "origin", remote]]) {
|
||||
expect(spawnSync("git", ["-C", dir, ...args], { encoding: "utf-8", timeout: 30_000, env: gitEnv() }).status).toBe(0);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
const old = new Date(Date.now() - 3 * 24 * 3600 * 1000);
|
||||
function kimi(id: string, workDir: string, updatedAt: number): void {
|
||||
const dir = join(home, ".kimi-code", "sessions", "ws-1", id);
|
||||
mkdirSync(join(dir, "agents", "main"), { recursive: true });
|
||||
writeFileSync(join(dir, "state.json"), JSON.stringify({ cwd: workDir, createdAt: updatedAt - 1000, updatedAt }));
|
||||
writeFileSync(join(dir, "agents", "main", "wire.jsonl"), JSON.stringify({ created_at: new Date(updatedAt).toISOString() }) + "\n");
|
||||
const index = join(home, ".kimi-code", "session_index.jsonl");
|
||||
const prev = existsSync(index) ? readFileSync(index, "utf-8") : "";
|
||||
writeFileSync(index, prev + JSON.stringify({ sessionId: id, sessionDir: dir, workDir }) + "\n");
|
||||
}
|
||||
function omp(name: string, cwd: string, mtime?: Date): string {
|
||||
const dir = join(home, ".omp", "agent", "sessions", "-work-proj");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const file = join(dir, name);
|
||||
writeFileSync(file, [JSON.stringify({ type: "session", cwd, timestamp: new Date().toISOString() }), JSON.stringify({ type: "message", timestamp: new Date().toISOString() })].join("\n") + "\n");
|
||||
if (mtime) utimesSync(file, mtime, mtime);
|
||||
return file;
|
||||
}
|
||||
function agy(name: string, mtime?: Date, walMtime?: Date): void {
|
||||
const dir = join(home, ".gemini", "antigravity-cli", "conversations");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, name), "");
|
||||
if (mtime) utimesSync(join(dir, name), mtime, mtime);
|
||||
if (walMtime) {
|
||||
writeFileSync(join(dir, `${name}-wal`), "");
|
||||
utimesSync(join(dir, `${name}-wal`), walMtime, walMtime);
|
||||
}
|
||||
}
|
||||
function discover(format: "json" | "summary" = "json") {
|
||||
const r = spawnSync("bun", ["run", scriptPath, "--since", "1d", "--format", format], {
|
||||
encoding: "utf-8",
|
||||
timeout: 60_000,
|
||||
env: { ...gitEnv(), HOME: home, USERPROFILE: home, CODEX_SESSIONS_DIR: join(home, "no-codex") },
|
||||
});
|
||||
expect(r.status).toBe(0);
|
||||
return { stdout: r.stdout, stderr: r.stderr };
|
||||
}
|
||||
|
||||
test("with no stores, every registered tool is reported with a zero count", () => {
|
||||
const json = JSON.parse(discover().stdout);
|
||||
for (const key of ["claude_code", "codex", "gemini", "kimi_code", "omp", "agy"]) expect(json.tools[key].total_sessions).toBe(0);
|
||||
expect(json.tools.agy.unattributed).toBe(true);
|
||||
expect(json.total_sessions).toBe(0);
|
||||
const { stdout, stderr } = discover("summary");
|
||||
expect(stdout).toContain("Kimi Code: 0");
|
||||
expect(stdout).toContain("OMP: 0");
|
||||
expect(stdout).toContain("Antigravity: 0)");
|
||||
expect(stderr).toContain("Kimi Code: 0");
|
||||
});
|
||||
|
||||
test("Kimi Code sessions count per repo by workDir, inside the window only", () => {
|
||||
const dir = repo("work/kimi-app", "https://github.com/acme/kimi-app.git");
|
||||
kimi("s-new", dir, Date.now());
|
||||
kimi("s-old", dir, old.getTime());
|
||||
const json = JSON.parse(discover().stdout);
|
||||
expect(json.tools.kimi_code).toEqual({ label: "Kimi Code", total_sessions: 1, repos: 1 });
|
||||
const r = json.repos.find((x: any) => x.remote === "https://github.com/acme/kimi-app");
|
||||
expect(r.sessions).toEqual({ claude_code: 0, codex: 0, gemini: 0, kimi_code: 1, omp: 0 });
|
||||
});
|
||||
|
||||
test("OMP sessions count by the cwd in the file and its mtime; a session created before the window but written in it counts; logs never count", () => {
|
||||
const dir = repo("work/omp-app", "https://github.com/acme/omp-app.git");
|
||||
omp("2026-01-01T00-00-00-000Z_aaaa.jsonl", dir);
|
||||
omp("2026-10-01T00-00-00-000Z_bbbb.jsonl", dir, old);
|
||||
mkdirSync(join(home, ".omp", "logs"), { recursive: true });
|
||||
writeFileSync(join(home, ".omp", "logs", "launch.log"), "proxy debug\n");
|
||||
const json = JSON.parse(discover().stdout);
|
||||
expect(json.tools.omp.total_sessions).toBe(1);
|
||||
expect(json.repos.find((x: any) => x.remote === "https://github.com/acme/omp-app").sessions.omp).toBe(1);
|
||||
});
|
||||
|
||||
test("Antigravity sessions count by the later of the .db and -wal mtimes, in total_sessions but in no repo", () => {
|
||||
agy("a.db");
|
||||
agy("b.db", old, new Date());
|
||||
agy("c.db", old);
|
||||
agy("notes.txt");
|
||||
const json = JSON.parse(discover().stdout);
|
||||
expect(json.tools.agy).toEqual({ label: "Antigravity", total_sessions: 2, repos: 0, unattributed: true });
|
||||
expect(json.total_sessions).toBe(2);
|
||||
expect(json.repos).toEqual([]);
|
||||
const summary = discover("summary").stdout;
|
||||
expect(summary).toContain("Sessions: 2 total (CC: 0, Codex: 0, Gemini: 0, Kimi Code: 0, OMP: 0, Antigravity: 2)");
|
||||
expect(summary).toContain("Antigravity sessions are not attributed to a repo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractCwdFromJsonl reads whole lines up to 1 MiB (PR #1169 bug #8, #1315)", () => {
|
||||
// Regression: the old 8KB cap landed mid-line on Claude Code sessions with
|
||||
// long headers, JSON.parse threw on the truncated tail, the catch
|
||||
|
||||
Reference in new issue
Block a user