@openthink/team 0.0.2 → 0.0.3

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command5 } from "commander";
4
+ import { Command as Command6 } from "commander";
5
5
 
6
6
  // src/commands/pull.ts
7
7
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -487,11 +487,20 @@ function writeConfig(config) {
487
487
  if (Object.keys(config.models).length > 0) {
488
488
  onDisk.models = config.models;
489
489
  }
490
+ if (!config.telemetry.enabled) {
491
+ onDisk.telemetry = config.telemetry;
492
+ }
490
493
  const body = JSON.stringify(onDisk, null, 2) + "\n";
491
494
  writeFileSync(configPath(), body);
492
495
  }
493
496
  function emptyConfig() {
494
- return { vaults: {}, default: null, stamp: null, models: {} };
497
+ return {
498
+ vaults: {},
499
+ default: null,
500
+ stamp: null,
501
+ models: {},
502
+ telemetry: { enabled: true }
503
+ };
495
504
  }
496
505
  function addVault(rawPath, options = {}) {
497
506
  const path = absolutise(rawPath);
@@ -590,9 +599,15 @@ function normalise(parsed) {
590
599
  vaults,
591
600
  default: def,
592
601
  stamp: normaliseStamp(obj.stamp),
593
- models: normaliseModels(obj.models)
602
+ models: normaliseModels(obj.models),
603
+ telemetry: normaliseTelemetry(obj.telemetry)
594
604
  };
595
605
  }
606
+ function normaliseTelemetry(value) {
607
+ if (!value || typeof value !== "object") return { enabled: true };
608
+ const v = value;
609
+ return { enabled: v.enabled !== false };
610
+ }
596
611
  function normaliseModels(value) {
597
612
  if (!value || typeof value !== "object") return {};
598
613
  const out = {};
@@ -697,6 +712,15 @@ function seedDefaultModelsIfEmpty() {
697
712
  writeConfig(config);
698
713
  return { action: "seeded", models: config.models };
699
714
  }
715
+ function getTelemetryEnabled() {
716
+ return readConfig().telemetry.enabled;
717
+ }
718
+ function setTelemetryEnabled(enabled) {
719
+ const config = readConfig();
720
+ config.telemetry = { enabled };
721
+ writeConfig(config);
722
+ return config.telemetry;
723
+ }
700
724
  function clearModel(phase) {
701
725
  if (!isPhase(phase)) {
702
726
  throw new Error(
@@ -1275,9 +1299,32 @@ enforce: ${s.enforce ? "on" : "off"}
1275
1299
  const lines = PHASES.map((p) => `${p.padEnd(15)} ${m[p] ?? "(unset)"}`);
1276
1300
  process.stdout.write(lines.join("\n") + "\n");
1277
1301
  });
1302
+ const telemetry = new Command("telemetry").description(
1303
+ "Manage per-phase telemetry recording (default: on)"
1304
+ );
1305
+ telemetry.command("set <on|off>").description("Turn per-phase telemetry recording on or off").action((flag) => {
1306
+ const lower = flag.toLowerCase();
1307
+ if (lower !== "on" && lower !== "off") {
1308
+ process.stderr.write(
1309
+ `oteam config telemetry set: expected on|off, got "${flag}"
1310
+ `
1311
+ );
1312
+ process.exit(2);
1313
+ }
1314
+ const next = setTelemetryEnabled(lower === "on");
1315
+ process.stdout.write(
1316
+ `\u2705 telemetry ${next.enabled ? "on" : "off"}
1317
+ `
1318
+ );
1319
+ });
1320
+ telemetry.command("show").description("Print whether telemetry recording is on").action(() => {
1321
+ process.stdout.write(`${getTelemetryEnabled() ? "on" : "off"}
1322
+ `);
1323
+ });
1278
1324
  config.addCommand(vault);
1279
1325
  config.addCommand(stamp);
1280
1326
  config.addCommand(models);
1327
+ config.addCommand(telemetry);
1281
1328
  return config;
1282
1329
  }
1283
1330
  function expectPhase(value) {
@@ -1961,18 +2008,345 @@ function openInEditor(path) {
1961
2008
  }
1962
2009
  }
1963
2010
 
1964
- // src/commands/ticket.ts
2011
+ // src/commands/telemetry.ts
1965
2012
  import { Command as Command4 } from "commander";
1966
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
2013
+
2014
+ // src/lib/telemetry.ts
2015
+ import {
2016
+ appendFileSync,
2017
+ existsSync as existsSync7,
2018
+ mkdirSync as mkdirSync6,
2019
+ readFileSync as readFileSync7
2020
+ } from "fs";
2021
+ import { homedir as homedir4 } from "os";
2022
+ import { join as join10 } from "path";
2023
+
2024
+ // src/lib/claude-session.ts
2025
+ import { readFileSync as readFileSync6 } from "fs";
1967
2026
  import { join as join9 } from "path";
2027
+ function encodeProjectDir(cwd) {
2028
+ return cwd.replace(/\//g, "-");
2029
+ }
2030
+ function findSessionFile(claudeConfigDir, cwd, sessionId) {
2031
+ return join9(
2032
+ claudeConfigDir,
2033
+ "projects",
2034
+ encodeProjectDir(cwd),
2035
+ `${sessionId}.jsonl`
2036
+ );
2037
+ }
2038
+ function parseSessionFile(path) {
2039
+ let raw;
2040
+ try {
2041
+ raw = readFileSync6(path, "utf8");
2042
+ } catch {
2043
+ return { tokens: {}, outcome: null };
2044
+ }
2045
+ return parseSessionJsonl(raw);
2046
+ }
2047
+ function parseSessionJsonl(raw) {
2048
+ const tokens = {};
2049
+ let lastAssistantText = "";
2050
+ for (const line of raw.split("\n")) {
2051
+ if (line.length === 0) continue;
2052
+ let entry;
2053
+ try {
2054
+ entry = JSON.parse(line);
2055
+ } catch {
2056
+ continue;
2057
+ }
2058
+ if (!entry || typeof entry !== "object") continue;
2059
+ const e = entry;
2060
+ if (e.type !== "assistant") continue;
2061
+ const message = e.message;
2062
+ if (!message || typeof message !== "object") continue;
2063
+ const m = message;
2064
+ if (m.usage && typeof m.usage === "object") {
2065
+ const u = m.usage;
2066
+ addIfFinite(tokens, "input", u.input_tokens);
2067
+ addIfFinite(tokens, "output", u.output_tokens);
2068
+ addIfFinite(tokens, "cache-write", u.cache_creation_input_tokens);
2069
+ addIfFinite(tokens, "cache-read", u.cache_read_input_tokens);
2070
+ }
2071
+ const text = extractAssistantText(m.content);
2072
+ if (text.length > 0) lastAssistantText = text;
2073
+ }
2074
+ return { tokens, outcome: detectOutcome(lastAssistantText) };
2075
+ }
2076
+ function extractAssistantText(content) {
2077
+ if (typeof content === "string") return content;
2078
+ if (!Array.isArray(content)) return "";
2079
+ let out = "";
2080
+ for (const part of content) {
2081
+ if (!part || typeof part !== "object") continue;
2082
+ const p = part;
2083
+ if (p.type === "text" && typeof p.text === "string") {
2084
+ out += (out.length > 0 ? "\n" : "") + p.text;
2085
+ }
2086
+ }
2087
+ return out;
2088
+ }
2089
+ function detectOutcome(text) {
2090
+ if (text.includes("\u2705 DONE")) return "done";
2091
+ if (text.includes("\u23F8\uFE0F PAUSED")) return "paused";
2092
+ if (text.includes("\u{1F6D1} BLOCKED")) return "failed";
2093
+ return null;
2094
+ }
2095
+ function addIfFinite(target, key, value) {
2096
+ if (typeof value !== "number" || !Number.isFinite(value)) return;
2097
+ target[key] = (target[key] ?? 0) + value;
2098
+ }
2099
+
2100
+ // src/lib/telemetry.ts
2101
+ function telemetryDir() {
2102
+ const override = process.env.OTEAM_TELEMETRY_DIR;
2103
+ if (override && override.length > 0) return override;
2104
+ return join10(homedir4(), ".open-team", "telemetry");
2105
+ }
2106
+ function runsPath(dir = telemetryDir()) {
2107
+ return join10(dir, "runs.jsonl");
2108
+ }
2109
+ function recordPhase(input) {
2110
+ try {
2111
+ if (!getTelemetryEnabled()) return;
2112
+ const endedAt = input.endedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2113
+ const wallClockMs = computeWallClockMs(input.startedAt, endedAt);
2114
+ const sessionFile = findSessionFile(
2115
+ resolveClaudeConfigDir(),
2116
+ input.cwd,
2117
+ input.sessionId
2118
+ );
2119
+ let tokens = {};
2120
+ let markerOutcome = null;
2121
+ if (existsSync7(sessionFile)) {
2122
+ const parsed = parseSessionFile(sessionFile);
2123
+ tokens = parsed.tokens;
2124
+ markerOutcome = parsed.outcome;
2125
+ }
2126
+ const outcome = input.exitCode !== 0 ? "failed" : markerOutcome ?? "unknown";
2127
+ const line = {
2128
+ ticket: input.ticket,
2129
+ phase: input.phase,
2130
+ model: input.model,
2131
+ "started-at": input.startedAt,
2132
+ "ended-at": endedAt,
2133
+ "wall-clock-ms": wallClockMs,
2134
+ tokens,
2135
+ outcome
2136
+ };
2137
+ const dir = telemetryDir();
2138
+ mkdirSync6(dir, { recursive: true });
2139
+ appendFileSync(runsPath(dir), JSON.stringify(line) + "\n");
2140
+ } catch (err) {
2141
+ const msg = err instanceof Error ? err.message : String(err);
2142
+ process.stderr.write(`oteam: telemetry record failed: ${msg}
2143
+ `);
2144
+ }
2145
+ }
2146
+ function resolveClaudeConfigDir() {
2147
+ const env = process.env.CLAUDE_CONFIG_DIR;
2148
+ if (env && env.length > 0) return env;
2149
+ return join10(homedir4(), ".claude");
2150
+ }
2151
+ function computeWallClockMs(startedAt, endedAt) {
2152
+ const startMs = Date.parse(startedAt);
2153
+ const endMs = Date.parse(endedAt);
2154
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return 0;
2155
+ return Math.max(0, endMs - startMs);
2156
+ }
2157
+ function readRuns(dir = telemetryDir()) {
2158
+ const path = runsPath(dir);
2159
+ if (!existsSync7(path)) return [];
2160
+ const raw = readFileSync7(path, "utf8");
2161
+ const out = [];
2162
+ for (const line of raw.split("\n")) {
2163
+ if (line.length === 0) continue;
2164
+ try {
2165
+ const parsed = JSON.parse(line);
2166
+ if (parsed && typeof parsed === "object") out.push(parsed);
2167
+ } catch {
2168
+ }
2169
+ }
2170
+ return out;
2171
+ }
2172
+ function summarize(runs, filter = {}) {
2173
+ const filtered = applyFilter(runs, filter);
2174
+ const buckets = /* @__PURE__ */ new Map();
2175
+ for (const r of filtered) {
2176
+ const key = `${r.phase} ${r.model}`;
2177
+ const bucket = buckets.get(key) ?? { phase: r.phase, model: r.model, rows: [] };
2178
+ bucket.rows.push(r);
2179
+ buckets.set(key, bucket);
2180
+ }
2181
+ const rows = [];
2182
+ for (const bucket of buckets.values()) {
2183
+ rows.push({
2184
+ phase: bucket.phase,
2185
+ model: bucket.model,
2186
+ count: bucket.rows.length,
2187
+ meanWallClockMs: meanWallClock(bucket.rows),
2188
+ meanTokens: meanTokens(bucket.rows)
2189
+ });
2190
+ }
2191
+ rows.sort(
2192
+ (a, b) => a.phase.localeCompare(b.phase) || a.model.localeCompare(b.model)
2193
+ );
2194
+ return rows;
2195
+ }
2196
+ function applyFilter(runs, filter) {
2197
+ let cutoffMs = null;
2198
+ if (typeof filter.days === "number" && filter.days > 0) {
2199
+ cutoffMs = Date.now() - filter.days * 24 * 60 * 60 * 1e3;
2200
+ }
2201
+ return runs.filter((r) => {
2202
+ if (filter.phase && r.phase !== filter.phase) return false;
2203
+ if (filter.model && r.model !== filter.model) return false;
2204
+ if (cutoffMs !== null) {
2205
+ const t = Date.parse(r["started-at"]);
2206
+ if (!Number.isFinite(t) || t < cutoffMs) return false;
2207
+ }
2208
+ return true;
2209
+ });
2210
+ }
2211
+ function meanWallClock(rows) {
2212
+ if (rows.length === 0) return 0;
2213
+ const sum = rows.reduce((acc, r) => acc + (r["wall-clock-ms"] ?? 0), 0);
2214
+ return Math.round(sum / rows.length);
2215
+ }
2216
+ function meanTokens(rows) {
2217
+ const keys = [
2218
+ "input",
2219
+ "output",
2220
+ "cache-read",
2221
+ "cache-write"
2222
+ ];
2223
+ const out = {};
2224
+ for (const k of keys) {
2225
+ let sum = 0;
2226
+ let n = 0;
2227
+ for (const r of rows) {
2228
+ const v = r.tokens?.[k];
2229
+ if (typeof v === "number" && Number.isFinite(v)) {
2230
+ sum += v;
2231
+ n += 1;
2232
+ }
2233
+ }
2234
+ if (n > 0) out[k] = Math.round(sum / n);
2235
+ }
2236
+ return out;
2237
+ }
2238
+ function tail(n, dir = telemetryDir()) {
2239
+ const all = readRuns(dir);
2240
+ if (n <= 0) return [];
2241
+ return all.slice(-n);
2242
+ }
2243
+
2244
+ // src/commands/telemetry.ts
2245
+ function buildTelemetryCommand() {
2246
+ const telemetry = new Command4("telemetry").description(
2247
+ "Per-phase wall-clock + token telemetry for role-pipeline spawns"
2248
+ );
2249
+ telemetry.command("summary").description(
2250
+ "Aggregate runs.jsonl by phase \xD7 model (count, mean wall-clock, mean tokens)"
2251
+ ).option("--days <n>", "Only include runs from the last N days", parsePositiveInt).option("--phase <name>", "Filter by phase (product|spike|implementation|qa)").option("--model <id>", "Filter by resolved model id").action(
2252
+ (opts) => {
2253
+ const runs = readRuns();
2254
+ if (runs.length === 0) {
2255
+ process.stdout.write(
2256
+ `(no telemetry yet \u2014 ${telemetryDir()}/runs.jsonl is empty or missing)
2257
+ `
2258
+ );
2259
+ return;
2260
+ }
2261
+ const rows = summarize(runs, opts);
2262
+ if (rows.length === 0) {
2263
+ process.stdout.write("(no rows match filters)\n");
2264
+ return;
2265
+ }
2266
+ process.stdout.write(formatSummary(rows) + "\n");
2267
+ }
2268
+ );
2269
+ telemetry.command("tail").description("Print the last N telemetry lines (default 20)").option("-n, --count <n>", "How many lines to print", parsePositiveInt, 20).action((opts) => {
2270
+ const lines = tail(opts.count);
2271
+ if (lines.length === 0) {
2272
+ process.stdout.write(
2273
+ `(no telemetry yet \u2014 ${telemetryDir()}/runs.jsonl is empty or missing)
2274
+ `
2275
+ );
2276
+ return;
2277
+ }
2278
+ for (const line of lines) {
2279
+ process.stdout.write(JSON.stringify(line) + "\n");
2280
+ }
2281
+ });
2282
+ telemetry.command("record", { hidden: true }).description("(internal) Record one phase's telemetry line").requiredOption("--ticket <id>", "Ticket id (e.g. AGT-108)").requiredOption("--phase <name>", "Phase name (product|spike|implementation|qa)").requiredOption("--model <id>", "Resolved model id passed to claude").requiredOption("--session <uuid>", "Session UUID passed to `claude --session-id`").requiredOption("--started-at <iso>", "ISO timestamp captured before spawning claude").requiredOption("--exit-code <n>", "claude's exit code", parseSignedInt).option("--cwd <path>", "Working directory the spawn ran in (defaults to $PWD)").action(
2283
+ (opts) => {
2284
+ recordPhase({
2285
+ ticket: opts.ticket,
2286
+ phase: opts.phase,
2287
+ model: opts.model,
2288
+ sessionId: opts.session,
2289
+ startedAt: opts.startedAt,
2290
+ exitCode: opts.exitCode,
2291
+ cwd: opts.cwd ?? process.cwd()
2292
+ });
2293
+ }
2294
+ );
2295
+ return telemetry;
2296
+ }
2297
+ function parsePositiveInt(raw) {
2298
+ const n = Number.parseInt(raw, 10);
2299
+ if (!Number.isFinite(n) || n <= 0) {
2300
+ process.stderr.write(`oteam telemetry: expected a positive integer, got "${raw}"
2301
+ `);
2302
+ process.exit(2);
2303
+ }
2304
+ return n;
2305
+ }
2306
+ function parseSignedInt(raw) {
2307
+ const n = Number.parseInt(raw, 10);
2308
+ if (!Number.isFinite(n)) {
2309
+ process.stderr.write(`oteam telemetry: expected an integer, got "${raw}"
2310
+ `);
2311
+ process.exit(2);
2312
+ }
2313
+ return n;
2314
+ }
2315
+ function formatSummary(rows) {
2316
+ const headers = ["phase", "model", "count", "mean_ms", "mean_in", "mean_out", "mean_cache_r", "mean_cache_w"];
2317
+ const data = rows.map((r) => [
2318
+ r.phase,
2319
+ r.model,
2320
+ String(r.count),
2321
+ String(r.meanWallClockMs),
2322
+ formatTokenField(r.meanTokens, "input"),
2323
+ formatTokenField(r.meanTokens, "output"),
2324
+ formatTokenField(r.meanTokens, "cache-read"),
2325
+ formatTokenField(r.meanTokens, "cache-write")
2326
+ ]);
2327
+ const widths = headers.map(
2328
+ (h, i) => Math.max(h.length, ...data.map((row) => (row[i] ?? "").length))
2329
+ );
2330
+ const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
2331
+ return [fmt(headers), ...data.map(fmt)].join("\n");
2332
+ }
2333
+ function formatTokenField(tokens, key) {
2334
+ const v = tokens[key];
2335
+ return typeof v === "number" ? String(v) : "-";
2336
+ }
2337
+
2338
+ // src/commands/ticket.ts
2339
+ import { Command as Command5 } from "commander";
2340
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
2341
+ import { join as join11 } from "path";
1968
2342
  function runTicketNew(opts) {
1969
2343
  const title = opts.title.trim();
1970
2344
  if (title.length === 0) {
1971
2345
  throw new Error("oteam ticket new: <title> must not be empty");
1972
2346
  }
1973
2347
  const vault = resolveVaultPath({ flagValue: opts.vault });
1974
- const triageDir = join9(vault, "tickets", "triage");
1975
- mkdirSync6(triageDir, { recursive: true });
2348
+ const triageDir = join11(vault, "tickets", "triage");
2349
+ mkdirSync7(triageDir, { recursive: true });
1976
2350
  const id = nextTicketID(vault);
1977
2351
  const slug = slugify(title);
1978
2352
  if (slug.length === 0) {
@@ -1980,8 +2354,8 @@ function runTicketNew(opts) {
1980
2354
  `oteam ticket new: title "${title}" produced an empty slug \u2014 use a title with at least one alphanumeric character`
1981
2355
  );
1982
2356
  }
1983
- const target = join9(triageDir, `${id}-${slug}.md`);
1984
- if (existsSync7(target)) {
2357
+ const target = join11(triageDir, `${id}-${slug}.md`);
2358
+ if (existsSync8(target)) {
1985
2359
  throw new Error(
1986
2360
  `oteam ticket new: target already exists at ${target} \u2014 ID scan collision`
1987
2361
  );
@@ -2003,7 +2377,7 @@ function collectLabel(value, prev = []) {
2003
2377
  return [...prev, value];
2004
2378
  }
2005
2379
  function buildTicketCommand() {
2006
- const ticket = new Command4("ticket").description(
2380
+ const ticket = new Command5("ticket").description(
2007
2381
  "Create vault tickets directly (without an external source)"
2008
2382
  );
2009
2383
  ticket.command("new <title>").description(
@@ -2036,13 +2410,14 @@ function buildTicketCommand() {
2036
2410
 
2037
2411
  // src/role-pipeline/runner.ts
2038
2412
  import { spawnSync as spawnSync4 } from "child_process";
2413
+ import { randomUUID } from "crypto";
2039
2414
  import { writeFileSync as writeFileSync7 } from "fs";
2040
2415
  import { tmpdir } from "os";
2041
- import { resolve as resolve4, basename as basename5, dirname as dirname2, join as join12 } from "path";
2416
+ import { resolve as resolve4, basename as basename5, dirname as dirname2, join as join14 } from "path";
2042
2417
 
2043
2418
  // src/lib/kitty.ts
2044
2419
  import { spawnSync as spawnSync2 } from "child_process";
2045
- import { existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
2420
+ import { existsSync as existsSync9, readdirSync as readdirSync5 } from "fs";
2046
2421
  var SOCKET_BASENAME = "kitty-claudini";
2047
2422
  var KNOWN_INSTANCES = ["personal", "work"];
2048
2423
  function isMacOS() {
@@ -2085,7 +2460,7 @@ function findKittySocket(kittyPath, preferring) {
2085
2460
  candidates.push(...pidSuffixed);
2086
2461
  }
2087
2462
  for (const path of candidates) {
2088
- if (!existsSync8(path)) continue;
2463
+ if (!existsSync9(path)) continue;
2089
2464
  const socket = `unix:${path}`;
2090
2465
  const r = spawnSync2(kittyPath, ["@", "--to", socket, "ls"], {
2091
2466
  encoding: "utf8"
@@ -2151,9 +2526,9 @@ function shellEscape(s) {
2151
2526
  }
2152
2527
 
2153
2528
  // src/lib/workspace.ts
2154
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync6, rmSync } from "fs";
2529
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync6, rmSync } from "fs";
2155
2530
  import { spawnSync as spawnSync3 } from "child_process";
2156
- import { basename as basename4, join as join10 } from "path";
2531
+ import { basename as basename4, join as join12 } from "path";
2157
2532
  function buildGithubUrl(repoSlug) {
2158
2533
  return `git@github.com:${repoSlug}.git`;
2159
2534
  }
@@ -2191,12 +2566,12 @@ function prepareAgentWorkspace(opts) {
2191
2566
  );
2192
2567
  }
2193
2568
  const root = opts.rootDir ?? WORKSPACE_ROOT;
2194
- mkdirSync7(root, { recursive: true });
2569
+ mkdirSync8(root, { recursive: true });
2195
2570
  if (opts.activeTicketIds) gcOrphanWorkspaces(root, opts.activeTicketIds);
2196
- const ticketDir = join10(root, opts.ticketId.toLowerCase());
2197
- const repoDir = join10(ticketDir, "repo");
2571
+ const ticketDir = join12(root, opts.ticketId.toLowerCase());
2572
+ const repoDir = join12(ticketDir, "repo");
2198
2573
  rmSync(ticketDir, { recursive: true, force: true });
2199
- mkdirSync7(ticketDir, { recursive: true });
2574
+ mkdirSync8(ticketDir, { recursive: true });
2200
2575
  const repoBasename = basename4(opts.repoSlug);
2201
2576
  const cloneRunner = opts.cloneRunner ?? defaultCloneRunner;
2202
2577
  if (opts.mode === "github") {
@@ -2247,7 +2622,7 @@ var defaultCloneRunner = (url, dest) => {
2247
2622
  };
2248
2623
  };
2249
2624
  function gcOrphanWorkspaces(root, activeTicketIds) {
2250
- if (!existsSync9(root)) return [];
2625
+ if (!existsSync10(root)) return [];
2251
2626
  const removed = [];
2252
2627
  let entries;
2253
2628
  try {
@@ -2258,7 +2633,7 @@ function gcOrphanWorkspaces(root, activeTicketIds) {
2258
2633
  for (const name of entries) {
2259
2634
  if (!ORPHAN_DIR_RE.test(name)) continue;
2260
2635
  if (activeTicketIds.has(name)) continue;
2261
- const target = join10(root, name);
2636
+ const target = join12(root, name);
2262
2637
  try {
2263
2638
  rmSync(target, { recursive: true, force: true });
2264
2639
  removed.push(target);
@@ -2269,22 +2644,22 @@ function gcOrphanWorkspaces(root, activeTicketIds) {
2269
2644
  }
2270
2645
 
2271
2646
  // src/role-pipeline/install-slash-command.ts
2272
- import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
2273
- import { homedir as homedir4 } from "os";
2274
- import { dirname, join as join11 } from "path";
2647
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync7, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
2648
+ import { homedir as homedir5 } from "os";
2649
+ import { dirname, join as join13 } from "path";
2275
2650
  import { fileURLToPath } from "url";
2276
2651
  var moduleDir = dirname(fileURLToPath(import.meta.url));
2277
- var BUNDLED_PROMPT = join11(moduleDir, "assign-ticket.md");
2652
+ var BUNDLED_PROMPT = join13(moduleDir, "assign-ticket.md");
2278
2653
  function installRolePipelineSlashCommand() {
2279
- if (!existsSync10(BUNDLED_PROMPT)) return;
2280
- const bundled = readFileSync6(BUNDLED_PROMPT);
2654
+ if (!existsSync11(BUNDLED_PROMPT)) return;
2655
+ const bundled = readFileSync8(BUNDLED_PROMPT);
2281
2656
  const targets = resolveTargetDirs();
2282
2657
  for (const dir of targets) {
2283
2658
  try {
2284
- mkdirSync8(dir, { recursive: true });
2285
- const target = join11(dir, "assign-ticket.md");
2286
- if (existsSync10(target)) {
2287
- const current = readFileSync6(target);
2659
+ mkdirSync9(dir, { recursive: true });
2660
+ const target = join13(dir, "assign-ticket.md");
2661
+ if (existsSync11(target)) {
2662
+ const current = readFileSync8(target);
2288
2663
  if (current.equals(bundled)) continue;
2289
2664
  }
2290
2665
  copyFileSync(BUNDLED_PROMPT, target);
@@ -2293,23 +2668,23 @@ function installRolePipelineSlashCommand() {
2293
2668
  }
2294
2669
  }
2295
2670
  function resolveTargetDirs() {
2296
- const home = homedir4();
2671
+ const home = homedir5();
2297
2672
  const dirs = /* @__PURE__ */ new Set();
2298
- dirs.add(join11(home, ".claude", "commands"));
2673
+ dirs.add(join13(home, ".claude", "commands"));
2299
2674
  const configDir2 = process.env.CLAUDE_CONFIG_DIR;
2300
2675
  if (configDir2 && configDir2.length > 0) {
2301
- dirs.add(join11(configDir2, "commands"));
2676
+ dirs.add(join13(configDir2, "commands"));
2302
2677
  }
2303
2678
  try {
2304
2679
  for (const name of readdirSync7(home)) {
2305
2680
  if (!name.startsWith(".claude-")) continue;
2306
- const candidate = join11(home, name);
2681
+ const candidate = join13(home, name);
2307
2682
  let isDir = false;
2308
2683
  try {
2309
2684
  isDir = statSync4(candidate).isDirectory();
2310
2685
  } catch {
2311
2686
  }
2312
- if (isDir) dirs.add(join11(candidate, "commands"));
2687
+ if (isDir) dirs.add(join13(candidate, "commands"));
2313
2688
  }
2314
2689
  } catch {
2315
2690
  }
@@ -2383,9 +2758,24 @@ async function assignTicket(opts) {
2383
2758
  }
2384
2759
  const projectContext = loadProjectContext(resolvedVault.path, ticket.project);
2385
2760
  const model = resolveRoleModel(ticket.state, config.models);
2761
+ const phase = phaseForState(ticket.state);
2762
+ const telemetry = phase !== null && getTelemetryEnabled() ? {
2763
+ ticketId: ticket.id,
2764
+ phase,
2765
+ sessionId: randomUUID(),
2766
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2767
+ } : null;
2386
2768
  const kittyPath = !opts.workInline && isMacOS() ? findKittyBinary() : null;
2387
2769
  if (!kittyPath) {
2388
- runInline(claudePath, ticketPath, resolvedVault.path, projectContext, workspace, model);
2770
+ runInline(
2771
+ claudePath,
2772
+ ticketPath,
2773
+ resolvedVault.path,
2774
+ projectContext,
2775
+ workspace,
2776
+ model,
2777
+ telemetry
2778
+ );
2389
2779
  return;
2390
2780
  }
2391
2781
  const monitored = opts.monitoredOrgs ?? readMonitoredOrgsFromEnv();
@@ -2396,7 +2786,15 @@ async function assignTicket(opts) {
2396
2786
  `oteam assign: no kitty socket reachable (preferring "${preferring}"); falling back to inline run.
2397
2787
  `
2398
2788
  );
2399
- runInline(claudePath, ticketPath, resolvedVault.path, projectContext, workspace, model);
2789
+ runInline(
2790
+ claudePath,
2791
+ ticketPath,
2792
+ resolvedVault.path,
2793
+ projectContext,
2794
+ workspace,
2795
+ model,
2796
+ telemetry
2797
+ );
2400
2798
  return;
2401
2799
  }
2402
2800
  const cwd = workspace?.path ?? dirname2(ticketPath);
@@ -2411,7 +2809,17 @@ async function assignTicket(opts) {
2411
2809
  const slashPrompt = `/assign-ticket ${escapedTicket}`;
2412
2810
  const escapedPrompt = shellEscape(slashPrompt);
2413
2811
  const projectFlag = projectContext ? ` --append-system-prompt "$(cat '${shellEscape(projectContext.tmpFile)}')"` : "";
2414
- const shellCmd = `${envPrefix}exec '${escapedClaude}' --dangerously-skip-permissions --model ${shellEscape(model)}${projectFlag} '${escapedPrompt}'`;
2812
+ const sessionFlag = telemetry ? ` --session-id '${shellEscape(telemetry.sessionId)}'` : "";
2813
+ const claudeCmd = `'${escapedClaude}' --dangerously-skip-permissions --model ${shellEscape(model)}${sessionFlag}${projectFlag} '${escapedPrompt}'`;
2814
+ const telemetryTail = telemetry ? buildTelemetryTail({
2815
+ oteamPath: findToolOnPath("oteam") ?? "oteam",
2816
+ ticketId: telemetry.ticketId,
2817
+ phase: telemetry.phase,
2818
+ model,
2819
+ sessionId: telemetry.sessionId,
2820
+ startedAt: telemetry.startedAt
2821
+ }) : "";
2822
+ const shellCmd = `${envPrefix}${claudeCmd}${telemetryTail}`;
2415
2823
  const result = kittyLaunch({
2416
2824
  socket,
2417
2825
  title,
@@ -2425,16 +2833,32 @@ async function assignTicket(opts) {
2425
2833
  );
2426
2834
  }
2427
2835
  }
2428
- function runInline(claudePath, ticketPath, vaultPath, projectContext, workspace, model) {
2836
+ function buildTelemetryTail(input) {
2837
+ const oteam = `'${shellEscape(input.oteamPath)}'`;
2838
+ const args = [
2839
+ `--ticket '${shellEscape(input.ticketId)}'`,
2840
+ `--phase '${shellEscape(input.phase)}'`,
2841
+ `--model '${shellEscape(input.model)}'`,
2842
+ `--session '${shellEscape(input.sessionId)}'`,
2843
+ `--started-at '${shellEscape(input.startedAt)}'`,
2844
+ `--exit-code "$EC"`
2845
+ ].join(" ");
2846
+ return `; EC=$?; ${oteam} telemetry record ${args} >/dev/null 2>&1 || true; exit "$EC"`;
2847
+ }
2848
+ function runInline(claudePath, ticketPath, vaultPath, projectContext, workspace, model, telemetry) {
2429
2849
  const args = [
2430
2850
  "--dangerously-skip-permissions",
2431
2851
  "--model",
2432
2852
  model
2433
2853
  ];
2854
+ if (telemetry) {
2855
+ args.push("--session-id", telemetry.sessionId);
2856
+ }
2434
2857
  if (projectContext) {
2435
2858
  args.push("--append-system-prompt", projectContext.content);
2436
2859
  }
2437
2860
  args.push(`/assign-ticket ${ticketPath}`);
2861
+ const cwd = workspace?.path ?? process.cwd();
2438
2862
  const r = spawnSync4(
2439
2863
  claudePath,
2440
2864
  args,
@@ -2444,6 +2868,17 @@ function runInline(claudePath, ticketPath, vaultPath, projectContext, workspace,
2444
2868
  env: { ...process.env, PRODUCT_VAULT_PATH: vaultPath }
2445
2869
  }
2446
2870
  );
2871
+ if (telemetry) {
2872
+ recordPhase({
2873
+ ticket: telemetry.ticketId,
2874
+ phase: telemetry.phase,
2875
+ model,
2876
+ sessionId: telemetry.sessionId,
2877
+ startedAt: telemetry.startedAt,
2878
+ exitCode: r.status ?? -1,
2879
+ cwd
2880
+ });
2881
+ }
2447
2882
  if (r.status != null && r.status !== 0) process.exit(r.status);
2448
2883
  }
2449
2884
  function collectActiveTicketIds(vaultPath) {
@@ -2468,7 +2903,7 @@ function loadProjectContext(vaultPath, projectId) {
2468
2903
  }
2469
2904
  const content = formatProjectContextForPrompt(project);
2470
2905
  const safeId = projectId.replace(/[^a-zA-Z0-9._-]/g, "_");
2471
- const tmpFile = join12(tmpdir(), `oteam-project-${safeId}.md`);
2906
+ const tmpFile = join14(tmpdir(), `oteam-project-${safeId}.md`);
2472
2907
  writeFileSync7(tmpFile, content, "utf8");
2473
2908
  return { tmpFile, content };
2474
2909
  }
@@ -2485,7 +2920,7 @@ function readMonitoredOrgsFromEnv() {
2485
2920
  }
2486
2921
 
2487
2922
  // src/index.ts
2488
- var program = new Command5();
2923
+ var program = new Command6();
2489
2924
  program.name("oteam").description(
2490
2925
  "Source-agnostic vault-driven role pipeline for spawning Claude agents against tickets"
2491
2926
  ).version("0.0.1");
@@ -2564,6 +2999,7 @@ program.command("archive <ticket-id>").description("Move a done ticket to archiv
2564
2999
  program.addCommand(buildConfigCommand());
2565
3000
  program.addCommand(buildInitCommand());
2566
3001
  program.addCommand(buildProjectCommand());
3002
+ program.addCommand(buildTelemetryCommand());
2567
3003
  program.addCommand(buildTicketCommand());
2568
3004
  program.parseAsync(process.argv).catch((err) => {
2569
3005
  process.stderr.write(`oteam: ${err.message}