@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/README.md +35 -0
- package/dist/index.js +478 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 {
|
|
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/
|
|
2011
|
+
// src/commands/telemetry.ts
|
|
1965
2012
|
import { Command as Command4 } from "commander";
|
|
1966
|
-
|
|
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 =
|
|
1975
|
-
|
|
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 =
|
|
1984
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
-
|
|
2569
|
+
mkdirSync8(root, { recursive: true });
|
|
2195
2570
|
if (opts.activeTicketIds) gcOrphanWorkspaces(root, opts.activeTicketIds);
|
|
2196
|
-
const ticketDir =
|
|
2197
|
-
const repoDir =
|
|
2571
|
+
const ticketDir = join12(root, opts.ticketId.toLowerCase());
|
|
2572
|
+
const repoDir = join12(ticketDir, "repo");
|
|
2198
2573
|
rmSync(ticketDir, { recursive: true, force: true });
|
|
2199
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
|
2273
|
-
import { homedir as
|
|
2274
|
-
import { dirname, join as
|
|
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 =
|
|
2652
|
+
var BUNDLED_PROMPT = join13(moduleDir, "assign-ticket.md");
|
|
2278
2653
|
function installRolePipelineSlashCommand() {
|
|
2279
|
-
if (!
|
|
2280
|
-
const bundled =
|
|
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
|
-
|
|
2285
|
-
const target =
|
|
2286
|
-
if (
|
|
2287
|
-
const current =
|
|
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 =
|
|
2671
|
+
const home = homedir5();
|
|
2297
2672
|
const dirs = /* @__PURE__ */ new Set();
|
|
2298
|
-
dirs.add(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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}
|