@openthink/team 0.0.2 → 0.0.4
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/assign-ticket.md +2 -0
- package/dist/index.js +616 -51
- 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";
|
|
@@ -250,6 +250,7 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
250
250
|
// src/lib/models.ts
|
|
251
251
|
var ROLE_PIPELINE_MODEL = "claude-opus-4-7";
|
|
252
252
|
var NORMALISER_MODEL = "claude-sonnet-4-6";
|
|
253
|
+
var HAIKU_PRODUCT_MODEL = "claude-haiku-4-5";
|
|
253
254
|
var PHASES = ["product", "spike", "implementation", "qa"];
|
|
254
255
|
var DEFAULT_MODELS = {
|
|
255
256
|
product: "claude-sonnet-4-6",
|
|
@@ -281,6 +282,55 @@ function resolveRoleModel(state, models) {
|
|
|
281
282
|
if (pinned && pinned.length > 0) return pinned;
|
|
282
283
|
return ROLE_PIPELINE_MODEL;
|
|
283
284
|
}
|
|
285
|
+
function acceptanceCriteriaIsPopulated(body) {
|
|
286
|
+
const lines = body.split("\n");
|
|
287
|
+
let inSection = false;
|
|
288
|
+
let inHtmlComment = false;
|
|
289
|
+
const numberedBullet = /^\s*\d+\.\s+\S/;
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
if (!inSection) {
|
|
292
|
+
if (/^##\s+Acceptance Criteria\s*$/.test(line)) {
|
|
293
|
+
inSection = true;
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (/^##\s+/.test(line)) return false;
|
|
298
|
+
let scan = line;
|
|
299
|
+
while (scan.length > 0) {
|
|
300
|
+
if (inHtmlComment) {
|
|
301
|
+
const close = scan.indexOf("-->");
|
|
302
|
+
if (close === -1) {
|
|
303
|
+
scan = "";
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
scan = scan.slice(close + 3);
|
|
307
|
+
inHtmlComment = false;
|
|
308
|
+
} else {
|
|
309
|
+
const open = scan.indexOf("<!--");
|
|
310
|
+
if (open === -1) break;
|
|
311
|
+
const before = scan.slice(0, open);
|
|
312
|
+
if (numberedBullet.test(before)) return true;
|
|
313
|
+
const rest = scan.slice(open + 4);
|
|
314
|
+
const close = rest.indexOf("-->");
|
|
315
|
+
if (close === -1) {
|
|
316
|
+
inHtmlComment = true;
|
|
317
|
+
scan = "";
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
scan = rest.slice(close + 3);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (inHtmlComment) continue;
|
|
324
|
+
if (numberedBullet.test(scan)) return true;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
function resolveModelForTicket(args) {
|
|
329
|
+
if (args.state === "triage" && args.sourceType === "manual" && args.productDownshift && acceptanceCriteriaIsPopulated(args.body)) {
|
|
330
|
+
return HAIKU_PRODUCT_MODEL;
|
|
331
|
+
}
|
|
332
|
+
return resolveRoleModel(args.state, args.models);
|
|
333
|
+
}
|
|
284
334
|
|
|
285
335
|
// src/lib/normalise.ts
|
|
286
336
|
var SYSTEM_PROMPT = `You normalise unstructured work-item payloads (GitHub issues, Linear tickets, etc.) into well-formed product-vault tickets.
|
|
@@ -458,6 +508,7 @@ import {
|
|
|
458
508
|
} from "fs";
|
|
459
509
|
import { homedir } from "os";
|
|
460
510
|
import { basename, isAbsolute, resolve, join as join2 } from "path";
|
|
511
|
+
var DEFAULT_PRODUCT_DOWNSHIFT = true;
|
|
461
512
|
function configDir() {
|
|
462
513
|
return join2(homedir(), ".open-team");
|
|
463
514
|
}
|
|
@@ -484,14 +535,28 @@ function writeConfig(config) {
|
|
|
484
535
|
default: config.default,
|
|
485
536
|
stamp: config.stamp
|
|
486
537
|
};
|
|
487
|
-
|
|
488
|
-
|
|
538
|
+
const onDiskModels = { ...config.models };
|
|
539
|
+
if (config.productDownshift !== DEFAULT_PRODUCT_DOWNSHIFT) {
|
|
540
|
+
onDiskModels.productDownshift = config.productDownshift;
|
|
541
|
+
}
|
|
542
|
+
if (Object.keys(onDiskModels).length > 0) {
|
|
543
|
+
onDisk.models = onDiskModels;
|
|
544
|
+
}
|
|
545
|
+
if (!config.telemetry.enabled) {
|
|
546
|
+
onDisk.telemetry = config.telemetry;
|
|
489
547
|
}
|
|
490
548
|
const body = JSON.stringify(onDisk, null, 2) + "\n";
|
|
491
549
|
writeFileSync(configPath(), body);
|
|
492
550
|
}
|
|
493
551
|
function emptyConfig() {
|
|
494
|
-
return {
|
|
552
|
+
return {
|
|
553
|
+
vaults: {},
|
|
554
|
+
default: null,
|
|
555
|
+
stamp: null,
|
|
556
|
+
models: {},
|
|
557
|
+
productDownshift: DEFAULT_PRODUCT_DOWNSHIFT,
|
|
558
|
+
telemetry: { enabled: true }
|
|
559
|
+
};
|
|
495
560
|
}
|
|
496
561
|
function addVault(rawPath, options = {}) {
|
|
497
562
|
const path = absolutise(rawPath);
|
|
@@ -590,9 +655,21 @@ function normalise(parsed) {
|
|
|
590
655
|
vaults,
|
|
591
656
|
default: def,
|
|
592
657
|
stamp: normaliseStamp(obj.stamp),
|
|
593
|
-
models: normaliseModels(obj.models)
|
|
658
|
+
models: normaliseModels(obj.models),
|
|
659
|
+
productDownshift: normaliseProductDownshift(obj.models),
|
|
660
|
+
telemetry: normaliseTelemetry(obj.telemetry)
|
|
594
661
|
};
|
|
595
662
|
}
|
|
663
|
+
function normaliseProductDownshift(value) {
|
|
664
|
+
if (!value || typeof value !== "object") return DEFAULT_PRODUCT_DOWNSHIFT;
|
|
665
|
+
const v = value;
|
|
666
|
+
return v.productDownshift !== false;
|
|
667
|
+
}
|
|
668
|
+
function normaliseTelemetry(value) {
|
|
669
|
+
if (!value || typeof value !== "object") return { enabled: true };
|
|
670
|
+
const v = value;
|
|
671
|
+
return { enabled: v.enabled !== false };
|
|
672
|
+
}
|
|
596
673
|
function normaliseModels(value) {
|
|
597
674
|
if (!value || typeof value !== "object") return {};
|
|
598
675
|
const out = {};
|
|
@@ -697,6 +774,24 @@ function seedDefaultModelsIfEmpty() {
|
|
|
697
774
|
writeConfig(config);
|
|
698
775
|
return { action: "seeded", models: config.models };
|
|
699
776
|
}
|
|
777
|
+
function getProductDownshift() {
|
|
778
|
+
return readConfig().productDownshift;
|
|
779
|
+
}
|
|
780
|
+
function setProductDownshift(enabled) {
|
|
781
|
+
const config = readConfig();
|
|
782
|
+
config.productDownshift = enabled;
|
|
783
|
+
writeConfig(config);
|
|
784
|
+
return config.productDownshift;
|
|
785
|
+
}
|
|
786
|
+
function getTelemetryEnabled() {
|
|
787
|
+
return readConfig().telemetry.enabled;
|
|
788
|
+
}
|
|
789
|
+
function setTelemetryEnabled(enabled) {
|
|
790
|
+
const config = readConfig();
|
|
791
|
+
config.telemetry = { enabled };
|
|
792
|
+
writeConfig(config);
|
|
793
|
+
return config.telemetry;
|
|
794
|
+
}
|
|
700
795
|
function clearModel(phase) {
|
|
701
796
|
if (!isPhase(phase)) {
|
|
702
797
|
throw new Error(
|
|
@@ -1275,9 +1370,54 @@ enforce: ${s.enforce ? "on" : "off"}
|
|
|
1275
1370
|
const lines = PHASES.map((p) => `${p.padEnd(15)} ${m[p] ?? "(unset)"}`);
|
|
1276
1371
|
process.stdout.write(lines.join("\n") + "\n");
|
|
1277
1372
|
});
|
|
1373
|
+
models.command("product-downshift <on|off|show>").description(
|
|
1374
|
+
"Toggle the AGT-107 Haiku-downshift heuristic for well-formed manual tickets (default: on)"
|
|
1375
|
+
).action((flag) => {
|
|
1376
|
+
const lower = flag.toLowerCase();
|
|
1377
|
+
if (lower === "show") {
|
|
1378
|
+
process.stdout.write(`${getProductDownshift() ? "on" : "off"}
|
|
1379
|
+
`);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (lower !== "on" && lower !== "off") {
|
|
1383
|
+
process.stderr.write(
|
|
1384
|
+
`oteam config models product-downshift: expected on|off|show, got "${flag}"
|
|
1385
|
+
`
|
|
1386
|
+
);
|
|
1387
|
+
process.exit(2);
|
|
1388
|
+
}
|
|
1389
|
+
const next = setProductDownshift(lower === "on");
|
|
1390
|
+
process.stdout.write(
|
|
1391
|
+
`\u2705 models.productDownshift = ${next ? "on" : "off"}
|
|
1392
|
+
`
|
|
1393
|
+
);
|
|
1394
|
+
});
|
|
1395
|
+
const telemetry = new Command("telemetry").description(
|
|
1396
|
+
"Manage per-phase telemetry recording (default: on)"
|
|
1397
|
+
);
|
|
1398
|
+
telemetry.command("set <on|off>").description("Turn per-phase telemetry recording on or off").action((flag) => {
|
|
1399
|
+
const lower = flag.toLowerCase();
|
|
1400
|
+
if (lower !== "on" && lower !== "off") {
|
|
1401
|
+
process.stderr.write(
|
|
1402
|
+
`oteam config telemetry set: expected on|off, got "${flag}"
|
|
1403
|
+
`
|
|
1404
|
+
);
|
|
1405
|
+
process.exit(2);
|
|
1406
|
+
}
|
|
1407
|
+
const next = setTelemetryEnabled(lower === "on");
|
|
1408
|
+
process.stdout.write(
|
|
1409
|
+
`\u2705 telemetry ${next.enabled ? "on" : "off"}
|
|
1410
|
+
`
|
|
1411
|
+
);
|
|
1412
|
+
});
|
|
1413
|
+
telemetry.command("show").description("Print whether telemetry recording is on").action(() => {
|
|
1414
|
+
process.stdout.write(`${getTelemetryEnabled() ? "on" : "off"}
|
|
1415
|
+
`);
|
|
1416
|
+
});
|
|
1278
1417
|
config.addCommand(vault);
|
|
1279
1418
|
config.addCommand(stamp);
|
|
1280
1419
|
config.addCommand(models);
|
|
1420
|
+
config.addCommand(telemetry);
|
|
1281
1421
|
return config;
|
|
1282
1422
|
}
|
|
1283
1423
|
function expectPhase(value) {
|
|
@@ -1961,18 +2101,345 @@ function openInEditor(path) {
|
|
|
1961
2101
|
}
|
|
1962
2102
|
}
|
|
1963
2103
|
|
|
1964
|
-
// src/commands/
|
|
2104
|
+
// src/commands/telemetry.ts
|
|
1965
2105
|
import { Command as Command4 } from "commander";
|
|
1966
|
-
|
|
2106
|
+
|
|
2107
|
+
// src/lib/telemetry.ts
|
|
2108
|
+
import {
|
|
2109
|
+
appendFileSync,
|
|
2110
|
+
existsSync as existsSync7,
|
|
2111
|
+
mkdirSync as mkdirSync6,
|
|
2112
|
+
readFileSync as readFileSync7
|
|
2113
|
+
} from "fs";
|
|
2114
|
+
import { homedir as homedir4 } from "os";
|
|
2115
|
+
import { join as join10 } from "path";
|
|
2116
|
+
|
|
2117
|
+
// src/lib/claude-session.ts
|
|
2118
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1967
2119
|
import { join as join9 } from "path";
|
|
2120
|
+
function encodeProjectDir(cwd) {
|
|
2121
|
+
return cwd.replace(/\//g, "-");
|
|
2122
|
+
}
|
|
2123
|
+
function findSessionFile(claudeConfigDir, cwd, sessionId) {
|
|
2124
|
+
return join9(
|
|
2125
|
+
claudeConfigDir,
|
|
2126
|
+
"projects",
|
|
2127
|
+
encodeProjectDir(cwd),
|
|
2128
|
+
`${sessionId}.jsonl`
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
function parseSessionFile(path) {
|
|
2132
|
+
let raw;
|
|
2133
|
+
try {
|
|
2134
|
+
raw = readFileSync6(path, "utf8");
|
|
2135
|
+
} catch {
|
|
2136
|
+
return { tokens: {}, outcome: null };
|
|
2137
|
+
}
|
|
2138
|
+
return parseSessionJsonl(raw);
|
|
2139
|
+
}
|
|
2140
|
+
function parseSessionJsonl(raw) {
|
|
2141
|
+
const tokens = {};
|
|
2142
|
+
let lastAssistantText = "";
|
|
2143
|
+
for (const line of raw.split("\n")) {
|
|
2144
|
+
if (line.length === 0) continue;
|
|
2145
|
+
let entry;
|
|
2146
|
+
try {
|
|
2147
|
+
entry = JSON.parse(line);
|
|
2148
|
+
} catch {
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2152
|
+
const e = entry;
|
|
2153
|
+
if (e.type !== "assistant") continue;
|
|
2154
|
+
const message = e.message;
|
|
2155
|
+
if (!message || typeof message !== "object") continue;
|
|
2156
|
+
const m = message;
|
|
2157
|
+
if (m.usage && typeof m.usage === "object") {
|
|
2158
|
+
const u = m.usage;
|
|
2159
|
+
addIfFinite(tokens, "input", u.input_tokens);
|
|
2160
|
+
addIfFinite(tokens, "output", u.output_tokens);
|
|
2161
|
+
addIfFinite(tokens, "cache-write", u.cache_creation_input_tokens);
|
|
2162
|
+
addIfFinite(tokens, "cache-read", u.cache_read_input_tokens);
|
|
2163
|
+
}
|
|
2164
|
+
const text = extractAssistantText(m.content);
|
|
2165
|
+
if (text.length > 0) lastAssistantText = text;
|
|
2166
|
+
}
|
|
2167
|
+
return { tokens, outcome: detectOutcome(lastAssistantText) };
|
|
2168
|
+
}
|
|
2169
|
+
function extractAssistantText(content) {
|
|
2170
|
+
if (typeof content === "string") return content;
|
|
2171
|
+
if (!Array.isArray(content)) return "";
|
|
2172
|
+
let out = "";
|
|
2173
|
+
for (const part of content) {
|
|
2174
|
+
if (!part || typeof part !== "object") continue;
|
|
2175
|
+
const p = part;
|
|
2176
|
+
if (p.type === "text" && typeof p.text === "string") {
|
|
2177
|
+
out += (out.length > 0 ? "\n" : "") + p.text;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return out;
|
|
2181
|
+
}
|
|
2182
|
+
function detectOutcome(text) {
|
|
2183
|
+
if (text.includes("\u2705 DONE")) return "done";
|
|
2184
|
+
if (text.includes("\u23F8\uFE0F PAUSED")) return "paused";
|
|
2185
|
+
if (text.includes("\u{1F6D1} BLOCKED")) return "failed";
|
|
2186
|
+
return null;
|
|
2187
|
+
}
|
|
2188
|
+
function addIfFinite(target, key, value) {
|
|
2189
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
2190
|
+
target[key] = (target[key] ?? 0) + value;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// src/lib/telemetry.ts
|
|
2194
|
+
function telemetryDir() {
|
|
2195
|
+
const override = process.env.OTEAM_TELEMETRY_DIR;
|
|
2196
|
+
if (override && override.length > 0) return override;
|
|
2197
|
+
return join10(homedir4(), ".open-team", "telemetry");
|
|
2198
|
+
}
|
|
2199
|
+
function runsPath(dir = telemetryDir()) {
|
|
2200
|
+
return join10(dir, "runs.jsonl");
|
|
2201
|
+
}
|
|
2202
|
+
function recordPhase(input) {
|
|
2203
|
+
try {
|
|
2204
|
+
if (!getTelemetryEnabled()) return;
|
|
2205
|
+
const endedAt = input.endedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2206
|
+
const wallClockMs = computeWallClockMs(input.startedAt, endedAt);
|
|
2207
|
+
const sessionFile = findSessionFile(
|
|
2208
|
+
resolveClaudeConfigDir(),
|
|
2209
|
+
input.cwd,
|
|
2210
|
+
input.sessionId
|
|
2211
|
+
);
|
|
2212
|
+
let tokens = {};
|
|
2213
|
+
let markerOutcome = null;
|
|
2214
|
+
if (existsSync7(sessionFile)) {
|
|
2215
|
+
const parsed = parseSessionFile(sessionFile);
|
|
2216
|
+
tokens = parsed.tokens;
|
|
2217
|
+
markerOutcome = parsed.outcome;
|
|
2218
|
+
}
|
|
2219
|
+
const outcome = input.exitCode !== 0 ? "failed" : markerOutcome ?? "unknown";
|
|
2220
|
+
const line = {
|
|
2221
|
+
ticket: input.ticket,
|
|
2222
|
+
phase: input.phase,
|
|
2223
|
+
model: input.model,
|
|
2224
|
+
"started-at": input.startedAt,
|
|
2225
|
+
"ended-at": endedAt,
|
|
2226
|
+
"wall-clock-ms": wallClockMs,
|
|
2227
|
+
tokens,
|
|
2228
|
+
outcome
|
|
2229
|
+
};
|
|
2230
|
+
const dir = telemetryDir();
|
|
2231
|
+
mkdirSync6(dir, { recursive: true });
|
|
2232
|
+
appendFileSync(runsPath(dir), JSON.stringify(line) + "\n");
|
|
2233
|
+
} catch (err) {
|
|
2234
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2235
|
+
process.stderr.write(`oteam: telemetry record failed: ${msg}
|
|
2236
|
+
`);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function resolveClaudeConfigDir() {
|
|
2240
|
+
const env = process.env.CLAUDE_CONFIG_DIR;
|
|
2241
|
+
if (env && env.length > 0) return env;
|
|
2242
|
+
return join10(homedir4(), ".claude");
|
|
2243
|
+
}
|
|
2244
|
+
function computeWallClockMs(startedAt, endedAt) {
|
|
2245
|
+
const startMs = Date.parse(startedAt);
|
|
2246
|
+
const endMs = Date.parse(endedAt);
|
|
2247
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return 0;
|
|
2248
|
+
return Math.max(0, endMs - startMs);
|
|
2249
|
+
}
|
|
2250
|
+
function readRuns(dir = telemetryDir()) {
|
|
2251
|
+
const path = runsPath(dir);
|
|
2252
|
+
if (!existsSync7(path)) return [];
|
|
2253
|
+
const raw = readFileSync7(path, "utf8");
|
|
2254
|
+
const out = [];
|
|
2255
|
+
for (const line of raw.split("\n")) {
|
|
2256
|
+
if (line.length === 0) continue;
|
|
2257
|
+
try {
|
|
2258
|
+
const parsed = JSON.parse(line);
|
|
2259
|
+
if (parsed && typeof parsed === "object") out.push(parsed);
|
|
2260
|
+
} catch {
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return out;
|
|
2264
|
+
}
|
|
2265
|
+
function summarize(runs, filter = {}) {
|
|
2266
|
+
const filtered = applyFilter(runs, filter);
|
|
2267
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
2268
|
+
for (const r of filtered) {
|
|
2269
|
+
const key = `${r.phase} ${r.model}`;
|
|
2270
|
+
const bucket = buckets.get(key) ?? { phase: r.phase, model: r.model, rows: [] };
|
|
2271
|
+
bucket.rows.push(r);
|
|
2272
|
+
buckets.set(key, bucket);
|
|
2273
|
+
}
|
|
2274
|
+
const rows = [];
|
|
2275
|
+
for (const bucket of buckets.values()) {
|
|
2276
|
+
rows.push({
|
|
2277
|
+
phase: bucket.phase,
|
|
2278
|
+
model: bucket.model,
|
|
2279
|
+
count: bucket.rows.length,
|
|
2280
|
+
meanWallClockMs: meanWallClock(bucket.rows),
|
|
2281
|
+
meanTokens: meanTokens(bucket.rows)
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
rows.sort(
|
|
2285
|
+
(a, b) => a.phase.localeCompare(b.phase) || a.model.localeCompare(b.model)
|
|
2286
|
+
);
|
|
2287
|
+
return rows;
|
|
2288
|
+
}
|
|
2289
|
+
function applyFilter(runs, filter) {
|
|
2290
|
+
let cutoffMs = null;
|
|
2291
|
+
if (typeof filter.days === "number" && filter.days > 0) {
|
|
2292
|
+
cutoffMs = Date.now() - filter.days * 24 * 60 * 60 * 1e3;
|
|
2293
|
+
}
|
|
2294
|
+
return runs.filter((r) => {
|
|
2295
|
+
if (filter.phase && r.phase !== filter.phase) return false;
|
|
2296
|
+
if (filter.model && r.model !== filter.model) return false;
|
|
2297
|
+
if (cutoffMs !== null) {
|
|
2298
|
+
const t = Date.parse(r["started-at"]);
|
|
2299
|
+
if (!Number.isFinite(t) || t < cutoffMs) return false;
|
|
2300
|
+
}
|
|
2301
|
+
return true;
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
function meanWallClock(rows) {
|
|
2305
|
+
if (rows.length === 0) return 0;
|
|
2306
|
+
const sum = rows.reduce((acc, r) => acc + (r["wall-clock-ms"] ?? 0), 0);
|
|
2307
|
+
return Math.round(sum / rows.length);
|
|
2308
|
+
}
|
|
2309
|
+
function meanTokens(rows) {
|
|
2310
|
+
const keys = [
|
|
2311
|
+
"input",
|
|
2312
|
+
"output",
|
|
2313
|
+
"cache-read",
|
|
2314
|
+
"cache-write"
|
|
2315
|
+
];
|
|
2316
|
+
const out = {};
|
|
2317
|
+
for (const k of keys) {
|
|
2318
|
+
let sum = 0;
|
|
2319
|
+
let n = 0;
|
|
2320
|
+
for (const r of rows) {
|
|
2321
|
+
const v = r.tokens?.[k];
|
|
2322
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
2323
|
+
sum += v;
|
|
2324
|
+
n += 1;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
if (n > 0) out[k] = Math.round(sum / n);
|
|
2328
|
+
}
|
|
2329
|
+
return out;
|
|
2330
|
+
}
|
|
2331
|
+
function tail(n, dir = telemetryDir()) {
|
|
2332
|
+
const all = readRuns(dir);
|
|
2333
|
+
if (n <= 0) return [];
|
|
2334
|
+
return all.slice(-n);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/commands/telemetry.ts
|
|
2338
|
+
function buildTelemetryCommand() {
|
|
2339
|
+
const telemetry = new Command4("telemetry").description(
|
|
2340
|
+
"Per-phase wall-clock + token telemetry for role-pipeline spawns"
|
|
2341
|
+
);
|
|
2342
|
+
telemetry.command("summary").description(
|
|
2343
|
+
"Aggregate runs.jsonl by phase \xD7 model (count, mean wall-clock, mean tokens)"
|
|
2344
|
+
).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(
|
|
2345
|
+
(opts) => {
|
|
2346
|
+
const runs = readRuns();
|
|
2347
|
+
if (runs.length === 0) {
|
|
2348
|
+
process.stdout.write(
|
|
2349
|
+
`(no telemetry yet \u2014 ${telemetryDir()}/runs.jsonl is empty or missing)
|
|
2350
|
+
`
|
|
2351
|
+
);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
const rows = summarize(runs, opts);
|
|
2355
|
+
if (rows.length === 0) {
|
|
2356
|
+
process.stdout.write("(no rows match filters)\n");
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
process.stdout.write(formatSummary(rows) + "\n");
|
|
2360
|
+
}
|
|
2361
|
+
);
|
|
2362
|
+
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) => {
|
|
2363
|
+
const lines = tail(opts.count);
|
|
2364
|
+
if (lines.length === 0) {
|
|
2365
|
+
process.stdout.write(
|
|
2366
|
+
`(no telemetry yet \u2014 ${telemetryDir()}/runs.jsonl is empty or missing)
|
|
2367
|
+
`
|
|
2368
|
+
);
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
for (const line of lines) {
|
|
2372
|
+
process.stdout.write(JSON.stringify(line) + "\n");
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
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(
|
|
2376
|
+
(opts) => {
|
|
2377
|
+
recordPhase({
|
|
2378
|
+
ticket: opts.ticket,
|
|
2379
|
+
phase: opts.phase,
|
|
2380
|
+
model: opts.model,
|
|
2381
|
+
sessionId: opts.session,
|
|
2382
|
+
startedAt: opts.startedAt,
|
|
2383
|
+
exitCode: opts.exitCode,
|
|
2384
|
+
cwd: opts.cwd ?? process.cwd()
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
);
|
|
2388
|
+
return telemetry;
|
|
2389
|
+
}
|
|
2390
|
+
function parsePositiveInt(raw) {
|
|
2391
|
+
const n = Number.parseInt(raw, 10);
|
|
2392
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
2393
|
+
process.stderr.write(`oteam telemetry: expected a positive integer, got "${raw}"
|
|
2394
|
+
`);
|
|
2395
|
+
process.exit(2);
|
|
2396
|
+
}
|
|
2397
|
+
return n;
|
|
2398
|
+
}
|
|
2399
|
+
function parseSignedInt(raw) {
|
|
2400
|
+
const n = Number.parseInt(raw, 10);
|
|
2401
|
+
if (!Number.isFinite(n)) {
|
|
2402
|
+
process.stderr.write(`oteam telemetry: expected an integer, got "${raw}"
|
|
2403
|
+
`);
|
|
2404
|
+
process.exit(2);
|
|
2405
|
+
}
|
|
2406
|
+
return n;
|
|
2407
|
+
}
|
|
2408
|
+
function formatSummary(rows) {
|
|
2409
|
+
const headers = ["phase", "model", "count", "mean_ms", "mean_in", "mean_out", "mean_cache_r", "mean_cache_w"];
|
|
2410
|
+
const data = rows.map((r) => [
|
|
2411
|
+
r.phase,
|
|
2412
|
+
r.model,
|
|
2413
|
+
String(r.count),
|
|
2414
|
+
String(r.meanWallClockMs),
|
|
2415
|
+
formatTokenField(r.meanTokens, "input"),
|
|
2416
|
+
formatTokenField(r.meanTokens, "output"),
|
|
2417
|
+
formatTokenField(r.meanTokens, "cache-read"),
|
|
2418
|
+
formatTokenField(r.meanTokens, "cache-write")
|
|
2419
|
+
]);
|
|
2420
|
+
const widths = headers.map(
|
|
2421
|
+
(h, i) => Math.max(h.length, ...data.map((row) => (row[i] ?? "").length))
|
|
2422
|
+
);
|
|
2423
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
|
|
2424
|
+
return [fmt(headers), ...data.map(fmt)].join("\n");
|
|
2425
|
+
}
|
|
2426
|
+
function formatTokenField(tokens, key) {
|
|
2427
|
+
const v = tokens[key];
|
|
2428
|
+
return typeof v === "number" ? String(v) : "-";
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// src/commands/ticket.ts
|
|
2432
|
+
import { Command as Command5 } from "commander";
|
|
2433
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
2434
|
+
import { join as join11 } from "path";
|
|
1968
2435
|
function runTicketNew(opts) {
|
|
1969
2436
|
const title = opts.title.trim();
|
|
1970
2437
|
if (title.length === 0) {
|
|
1971
2438
|
throw new Error("oteam ticket new: <title> must not be empty");
|
|
1972
2439
|
}
|
|
1973
2440
|
const vault = resolveVaultPath({ flagValue: opts.vault });
|
|
1974
|
-
const triageDir =
|
|
1975
|
-
|
|
2441
|
+
const triageDir = join11(vault, "tickets", "triage");
|
|
2442
|
+
mkdirSync7(triageDir, { recursive: true });
|
|
1976
2443
|
const id = nextTicketID(vault);
|
|
1977
2444
|
const slug = slugify(title);
|
|
1978
2445
|
if (slug.length === 0) {
|
|
@@ -1980,8 +2447,8 @@ function runTicketNew(opts) {
|
|
|
1980
2447
|
`oteam ticket new: title "${title}" produced an empty slug \u2014 use a title with at least one alphanumeric character`
|
|
1981
2448
|
);
|
|
1982
2449
|
}
|
|
1983
|
-
const target =
|
|
1984
|
-
if (
|
|
2450
|
+
const target = join11(triageDir, `${id}-${slug}.md`);
|
|
2451
|
+
if (existsSync8(target)) {
|
|
1985
2452
|
throw new Error(
|
|
1986
2453
|
`oteam ticket new: target already exists at ${target} \u2014 ID scan collision`
|
|
1987
2454
|
);
|
|
@@ -2003,7 +2470,7 @@ function collectLabel(value, prev = []) {
|
|
|
2003
2470
|
return [...prev, value];
|
|
2004
2471
|
}
|
|
2005
2472
|
function buildTicketCommand() {
|
|
2006
|
-
const ticket = new
|
|
2473
|
+
const ticket = new Command5("ticket").description(
|
|
2007
2474
|
"Create vault tickets directly (without an external source)"
|
|
2008
2475
|
);
|
|
2009
2476
|
ticket.command("new <title>").description(
|
|
@@ -2036,13 +2503,14 @@ function buildTicketCommand() {
|
|
|
2036
2503
|
|
|
2037
2504
|
// src/role-pipeline/runner.ts
|
|
2038
2505
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2039
|
-
import {
|
|
2506
|
+
import { randomUUID } from "crypto";
|
|
2507
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
|
|
2040
2508
|
import { tmpdir } from "os";
|
|
2041
|
-
import { resolve as resolve4, basename as basename5, dirname as dirname2, join as
|
|
2509
|
+
import { resolve as resolve4, basename as basename5, dirname as dirname2, join as join14 } from "path";
|
|
2042
2510
|
|
|
2043
2511
|
// src/lib/kitty.ts
|
|
2044
2512
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2045
|
-
import { existsSync as
|
|
2513
|
+
import { existsSync as existsSync9, readdirSync as readdirSync5 } from "fs";
|
|
2046
2514
|
var SOCKET_BASENAME = "kitty-claudini";
|
|
2047
2515
|
var KNOWN_INSTANCES = ["personal", "work"];
|
|
2048
2516
|
function isMacOS() {
|
|
@@ -2085,7 +2553,7 @@ function findKittySocket(kittyPath, preferring) {
|
|
|
2085
2553
|
candidates.push(...pidSuffixed);
|
|
2086
2554
|
}
|
|
2087
2555
|
for (const path of candidates) {
|
|
2088
|
-
if (!
|
|
2556
|
+
if (!existsSync9(path)) continue;
|
|
2089
2557
|
const socket = `unix:${path}`;
|
|
2090
2558
|
const r = spawnSync2(kittyPath, ["@", "--to", socket, "ls"], {
|
|
2091
2559
|
encoding: "utf8"
|
|
@@ -2151,9 +2619,9 @@ function shellEscape(s) {
|
|
|
2151
2619
|
}
|
|
2152
2620
|
|
|
2153
2621
|
// src/lib/workspace.ts
|
|
2154
|
-
import { existsSync as
|
|
2622
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync6, rmSync } from "fs";
|
|
2155
2623
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
2156
|
-
import { basename as basename4, join as
|
|
2624
|
+
import { basename as basename4, join as join12 } from "path";
|
|
2157
2625
|
function buildGithubUrl(repoSlug) {
|
|
2158
2626
|
return `git@github.com:${repoSlug}.git`;
|
|
2159
2627
|
}
|
|
@@ -2191,12 +2659,12 @@ function prepareAgentWorkspace(opts) {
|
|
|
2191
2659
|
);
|
|
2192
2660
|
}
|
|
2193
2661
|
const root = opts.rootDir ?? WORKSPACE_ROOT;
|
|
2194
|
-
|
|
2662
|
+
mkdirSync8(root, { recursive: true });
|
|
2195
2663
|
if (opts.activeTicketIds) gcOrphanWorkspaces(root, opts.activeTicketIds);
|
|
2196
|
-
const ticketDir =
|
|
2197
|
-
const repoDir =
|
|
2664
|
+
const ticketDir = join12(root, opts.ticketId.toLowerCase());
|
|
2665
|
+
const repoDir = join12(ticketDir, "repo");
|
|
2198
2666
|
rmSync(ticketDir, { recursive: true, force: true });
|
|
2199
|
-
|
|
2667
|
+
mkdirSync8(ticketDir, { recursive: true });
|
|
2200
2668
|
const repoBasename = basename4(opts.repoSlug);
|
|
2201
2669
|
const cloneRunner = opts.cloneRunner ?? defaultCloneRunner;
|
|
2202
2670
|
if (opts.mode === "github") {
|
|
@@ -2247,7 +2715,7 @@ var defaultCloneRunner = (url, dest) => {
|
|
|
2247
2715
|
};
|
|
2248
2716
|
};
|
|
2249
2717
|
function gcOrphanWorkspaces(root, activeTicketIds) {
|
|
2250
|
-
if (!
|
|
2718
|
+
if (!existsSync10(root)) return [];
|
|
2251
2719
|
const removed = [];
|
|
2252
2720
|
let entries;
|
|
2253
2721
|
try {
|
|
@@ -2258,7 +2726,7 @@ function gcOrphanWorkspaces(root, activeTicketIds) {
|
|
|
2258
2726
|
for (const name of entries) {
|
|
2259
2727
|
if (!ORPHAN_DIR_RE.test(name)) continue;
|
|
2260
2728
|
if (activeTicketIds.has(name)) continue;
|
|
2261
|
-
const target =
|
|
2729
|
+
const target = join12(root, name);
|
|
2262
2730
|
try {
|
|
2263
2731
|
rmSync(target, { recursive: true, force: true });
|
|
2264
2732
|
removed.push(target);
|
|
@@ -2269,22 +2737,22 @@ function gcOrphanWorkspaces(root, activeTicketIds) {
|
|
|
2269
2737
|
}
|
|
2270
2738
|
|
|
2271
2739
|
// src/role-pipeline/install-slash-command.ts
|
|
2272
|
-
import { copyFileSync, existsSync as
|
|
2273
|
-
import { homedir as
|
|
2274
|
-
import { dirname, join as
|
|
2740
|
+
import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync7, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
2741
|
+
import { homedir as homedir5 } from "os";
|
|
2742
|
+
import { dirname, join as join13 } from "path";
|
|
2275
2743
|
import { fileURLToPath } from "url";
|
|
2276
2744
|
var moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
2277
|
-
var BUNDLED_PROMPT =
|
|
2745
|
+
var BUNDLED_PROMPT = join13(moduleDir, "assign-ticket.md");
|
|
2278
2746
|
function installRolePipelineSlashCommand() {
|
|
2279
|
-
if (!
|
|
2280
|
-
const bundled =
|
|
2747
|
+
if (!existsSync11(BUNDLED_PROMPT)) return;
|
|
2748
|
+
const bundled = readFileSync8(BUNDLED_PROMPT);
|
|
2281
2749
|
const targets = resolveTargetDirs();
|
|
2282
2750
|
for (const dir of targets) {
|
|
2283
2751
|
try {
|
|
2284
|
-
|
|
2285
|
-
const target =
|
|
2286
|
-
if (
|
|
2287
|
-
const current =
|
|
2752
|
+
mkdirSync9(dir, { recursive: true });
|
|
2753
|
+
const target = join13(dir, "assign-ticket.md");
|
|
2754
|
+
if (existsSync11(target)) {
|
|
2755
|
+
const current = readFileSync8(target);
|
|
2288
2756
|
if (current.equals(bundled)) continue;
|
|
2289
2757
|
}
|
|
2290
2758
|
copyFileSync(BUNDLED_PROMPT, target);
|
|
@@ -2293,23 +2761,23 @@ function installRolePipelineSlashCommand() {
|
|
|
2293
2761
|
}
|
|
2294
2762
|
}
|
|
2295
2763
|
function resolveTargetDirs() {
|
|
2296
|
-
const home =
|
|
2764
|
+
const home = homedir5();
|
|
2297
2765
|
const dirs = /* @__PURE__ */ new Set();
|
|
2298
|
-
dirs.add(
|
|
2766
|
+
dirs.add(join13(home, ".claude", "commands"));
|
|
2299
2767
|
const configDir2 = process.env.CLAUDE_CONFIG_DIR;
|
|
2300
2768
|
if (configDir2 && configDir2.length > 0) {
|
|
2301
|
-
dirs.add(
|
|
2769
|
+
dirs.add(join13(configDir2, "commands"));
|
|
2302
2770
|
}
|
|
2303
2771
|
try {
|
|
2304
2772
|
for (const name of readdirSync7(home)) {
|
|
2305
2773
|
if (!name.startsWith(".claude-")) continue;
|
|
2306
|
-
const candidate =
|
|
2774
|
+
const candidate = join13(home, name);
|
|
2307
2775
|
let isDir = false;
|
|
2308
2776
|
try {
|
|
2309
2777
|
isDir = statSync4(candidate).isDirectory();
|
|
2310
2778
|
} catch {
|
|
2311
2779
|
}
|
|
2312
|
-
if (isDir) dirs.add(
|
|
2780
|
+
if (isDir) dirs.add(join13(candidate, "commands"));
|
|
2313
2781
|
}
|
|
2314
2782
|
} catch {
|
|
2315
2783
|
}
|
|
@@ -2382,10 +2850,34 @@ async function assignTicket(opts) {
|
|
|
2382
2850
|
}
|
|
2383
2851
|
}
|
|
2384
2852
|
const projectContext = loadProjectContext(resolvedVault.path, ticket.project);
|
|
2385
|
-
const
|
|
2853
|
+
const ticketBody = readTicketBody(ticketPath);
|
|
2854
|
+
const model = resolveModelForTicket({
|
|
2855
|
+
state: ticket.state,
|
|
2856
|
+
sourceType: ticket.source.type,
|
|
2857
|
+
body: ticketBody,
|
|
2858
|
+
productDownshift: config.productDownshift,
|
|
2859
|
+
models: config.models
|
|
2860
|
+
});
|
|
2861
|
+
const haikuDownshift = model === HAIKU_PRODUCT_MODEL && ticket.state === "triage";
|
|
2862
|
+
const systemPrompt = composeSystemPrompt(ticket.id, projectContext, haikuDownshift);
|
|
2863
|
+
const phase = phaseForState(ticket.state);
|
|
2864
|
+
const telemetry = phase !== null && getTelemetryEnabled() ? {
|
|
2865
|
+
ticketId: ticket.id,
|
|
2866
|
+
phase,
|
|
2867
|
+
sessionId: randomUUID(),
|
|
2868
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2869
|
+
} : null;
|
|
2386
2870
|
const kittyPath = !opts.workInline && isMacOS() ? findKittyBinary() : null;
|
|
2387
2871
|
if (!kittyPath) {
|
|
2388
|
-
runInline(
|
|
2872
|
+
runInline(
|
|
2873
|
+
claudePath,
|
|
2874
|
+
ticketPath,
|
|
2875
|
+
resolvedVault.path,
|
|
2876
|
+
systemPrompt,
|
|
2877
|
+
workspace,
|
|
2878
|
+
model,
|
|
2879
|
+
telemetry
|
|
2880
|
+
);
|
|
2389
2881
|
return;
|
|
2390
2882
|
}
|
|
2391
2883
|
const monitored = opts.monitoredOrgs ?? readMonitoredOrgsFromEnv();
|
|
@@ -2396,7 +2888,15 @@ async function assignTicket(opts) {
|
|
|
2396
2888
|
`oteam assign: no kitty socket reachable (preferring "${preferring}"); falling back to inline run.
|
|
2397
2889
|
`
|
|
2398
2890
|
);
|
|
2399
|
-
runInline(
|
|
2891
|
+
runInline(
|
|
2892
|
+
claudePath,
|
|
2893
|
+
ticketPath,
|
|
2894
|
+
resolvedVault.path,
|
|
2895
|
+
systemPrompt,
|
|
2896
|
+
workspace,
|
|
2897
|
+
model,
|
|
2898
|
+
telemetry
|
|
2899
|
+
);
|
|
2400
2900
|
return;
|
|
2401
2901
|
}
|
|
2402
2902
|
const cwd = workspace?.path ?? dirname2(ticketPath);
|
|
@@ -2410,8 +2910,18 @@ async function assignTicket(opts) {
|
|
|
2410
2910
|
const escapedTicket = shellEscape(ticketPath);
|
|
2411
2911
|
const slashPrompt = `/assign-ticket ${escapedTicket}`;
|
|
2412
2912
|
const escapedPrompt = shellEscape(slashPrompt);
|
|
2413
|
-
const projectFlag =
|
|
2414
|
-
const
|
|
2913
|
+
const projectFlag = systemPrompt ? ` --append-system-prompt "$(cat '${shellEscape(systemPrompt.tmpFile)}')"` : "";
|
|
2914
|
+
const sessionFlag = telemetry ? ` --session-id '${shellEscape(telemetry.sessionId)}'` : "";
|
|
2915
|
+
const claudeCmd = `'${escapedClaude}' --dangerously-skip-permissions --model ${shellEscape(model)}${sessionFlag}${projectFlag} '${escapedPrompt}'`;
|
|
2916
|
+
const telemetryTail = telemetry ? buildTelemetryTail({
|
|
2917
|
+
oteamPath: findToolOnPath("oteam") ?? "oteam",
|
|
2918
|
+
ticketId: telemetry.ticketId,
|
|
2919
|
+
phase: telemetry.phase,
|
|
2920
|
+
model,
|
|
2921
|
+
sessionId: telemetry.sessionId,
|
|
2922
|
+
startedAt: telemetry.startedAt
|
|
2923
|
+
}) : "";
|
|
2924
|
+
const shellCmd = `${envPrefix}${claudeCmd}${telemetryTail}`;
|
|
2415
2925
|
const result = kittyLaunch({
|
|
2416
2926
|
socket,
|
|
2417
2927
|
title,
|
|
@@ -2425,16 +2935,32 @@ async function assignTicket(opts) {
|
|
|
2425
2935
|
);
|
|
2426
2936
|
}
|
|
2427
2937
|
}
|
|
2428
|
-
function
|
|
2938
|
+
function buildTelemetryTail(input) {
|
|
2939
|
+
const oteam = `'${shellEscape(input.oteamPath)}'`;
|
|
2940
|
+
const args = [
|
|
2941
|
+
`--ticket '${shellEscape(input.ticketId)}'`,
|
|
2942
|
+
`--phase '${shellEscape(input.phase)}'`,
|
|
2943
|
+
`--model '${shellEscape(input.model)}'`,
|
|
2944
|
+
`--session '${shellEscape(input.sessionId)}'`,
|
|
2945
|
+
`--started-at '${shellEscape(input.startedAt)}'`,
|
|
2946
|
+
`--exit-code "$EC"`
|
|
2947
|
+
].join(" ");
|
|
2948
|
+
return `; EC=$?; ${oteam} telemetry record ${args} >/dev/null 2>&1 || true; exit "$EC"`;
|
|
2949
|
+
}
|
|
2950
|
+
function runInline(claudePath, ticketPath, vaultPath, systemPrompt, workspace, model, telemetry) {
|
|
2429
2951
|
const args = [
|
|
2430
2952
|
"--dangerously-skip-permissions",
|
|
2431
2953
|
"--model",
|
|
2432
2954
|
model
|
|
2433
2955
|
];
|
|
2434
|
-
if (
|
|
2435
|
-
args.push("--
|
|
2956
|
+
if (telemetry) {
|
|
2957
|
+
args.push("--session-id", telemetry.sessionId);
|
|
2958
|
+
}
|
|
2959
|
+
if (systemPrompt) {
|
|
2960
|
+
args.push("--append-system-prompt", systemPrompt.content);
|
|
2436
2961
|
}
|
|
2437
2962
|
args.push(`/assign-ticket ${ticketPath}`);
|
|
2963
|
+
const cwd = workspace?.path ?? process.cwd();
|
|
2438
2964
|
const r = spawnSync4(
|
|
2439
2965
|
claudePath,
|
|
2440
2966
|
args,
|
|
@@ -2444,6 +2970,17 @@ function runInline(claudePath, ticketPath, vaultPath, projectContext, workspace,
|
|
|
2444
2970
|
env: { ...process.env, PRODUCT_VAULT_PATH: vaultPath }
|
|
2445
2971
|
}
|
|
2446
2972
|
);
|
|
2973
|
+
if (telemetry) {
|
|
2974
|
+
recordPhase({
|
|
2975
|
+
ticket: telemetry.ticketId,
|
|
2976
|
+
phase: telemetry.phase,
|
|
2977
|
+
model,
|
|
2978
|
+
sessionId: telemetry.sessionId,
|
|
2979
|
+
startedAt: telemetry.startedAt,
|
|
2980
|
+
exitCode: r.status ?? -1,
|
|
2981
|
+
cwd
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2447
2984
|
if (r.status != null && r.status !== 0) process.exit(r.status);
|
|
2448
2985
|
}
|
|
2449
2986
|
function collectActiveTicketIds(vaultPath) {
|
|
@@ -2466,12 +3003,39 @@ function loadProjectContext(vaultPath, projectId) {
|
|
|
2466
3003
|
);
|
|
2467
3004
|
return null;
|
|
2468
3005
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3006
|
+
return formatProjectContextForPrompt(project);
|
|
3007
|
+
}
|
|
3008
|
+
function composeSystemPrompt(ticketId, projectContext, haikuDownshift) {
|
|
3009
|
+
const parts = [];
|
|
3010
|
+
if (projectContext) parts.push(projectContext);
|
|
3011
|
+
if (haikuDownshift) parts.push(haikuDownshiftPromptHint());
|
|
3012
|
+
if (parts.length === 0) return null;
|
|
3013
|
+
const content = parts.join("\n\n");
|
|
3014
|
+
const safeId = ticketId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3015
|
+
const tmpFile = join14(tmpdir(), `oteam-prompt-${safeId}.md`);
|
|
2472
3016
|
writeFileSync7(tmpFile, content, "utf8");
|
|
2473
3017
|
return { tmpFile, content };
|
|
2474
3018
|
}
|
|
3019
|
+
function haikuDownshiftPromptHint() {
|
|
3020
|
+
return [
|
|
3021
|
+
"# Product agent: haiku-downshift heuristic active",
|
|
3022
|
+
"",
|
|
3023
|
+
"AGT-107: this ticket is a well-formed manual ticket (source.type=manual + populated `## Acceptance Criteria`), so the runner spawned you on Haiku 4.5 instead of the configured Product model. The heuristic exists to handle structural-cleanup cases cheaply; full synthesis still belongs on the configured Product model.",
|
|
3024
|
+
"",
|
|
3025
|
+
"When you advance the ticket, write the comment header as:",
|
|
3026
|
+
"",
|
|
3027
|
+
" ### YYYY-MM-DD \u2014 Product agent (haiku-downshift)",
|
|
3028
|
+
"",
|
|
3029
|
+
"instead of the standard `### YYYY-MM-DD \u2014 Product agent`. That makes the heuristic visible in the ticket's audit trail."
|
|
3030
|
+
].join("\n");
|
|
3031
|
+
}
|
|
3032
|
+
function readTicketBody(path) {
|
|
3033
|
+
try {
|
|
3034
|
+
return readFileSync9(path, "utf8");
|
|
3035
|
+
} catch {
|
|
3036
|
+
return "";
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
2475
3039
|
function findToolOnPath(name) {
|
|
2476
3040
|
const r = spawnSync4("/usr/bin/env", ["which", name], { encoding: "utf8" });
|
|
2477
3041
|
if (r.status !== 0) return null;
|
|
@@ -2485,7 +3049,7 @@ function readMonitoredOrgsFromEnv() {
|
|
|
2485
3049
|
}
|
|
2486
3050
|
|
|
2487
3051
|
// src/index.ts
|
|
2488
|
-
var program = new
|
|
3052
|
+
var program = new Command6();
|
|
2489
3053
|
program.name("oteam").description(
|
|
2490
3054
|
"Source-agnostic vault-driven role pipeline for spawning Claude agents against tickets"
|
|
2491
3055
|
).version("0.0.1");
|
|
@@ -2564,6 +3128,7 @@ program.command("archive <ticket-id>").description("Move a done ticket to archiv
|
|
|
2564
3128
|
program.addCommand(buildConfigCommand());
|
|
2565
3129
|
program.addCommand(buildInitCommand());
|
|
2566
3130
|
program.addCommand(buildProjectCommand());
|
|
3131
|
+
program.addCommand(buildTelemetryCommand());
|
|
2567
3132
|
program.addCommand(buildTicketCommand());
|
|
2568
3133
|
program.parseAsync(process.argv).catch((err) => {
|
|
2569
3134
|
process.stderr.write(`oteam: ${err.message}
|