@bli-cockpit/cli 0.2.111 → 0.2.112

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.
@@ -139,6 +139,8 @@ function sessionReportEntry(result, context) {
139
139
  ? (context.claudePriorDurablePointers.get(result.session_id) ?? null)
140
140
  : null;
141
141
  return {
142
+ ticket_id: context.ticketByWorktree?.get(result.worktree?.worktree_fingerprint ?? "") ?? null,
143
+ ticket_binding_source: context.ticketByWorktree?.has(result.worktree?.worktree_fingerprint ?? "") ? "explicit_bind" : null,
142
144
  codex_session_id: result.session_id,
143
145
  source: result.source,
144
146
  observed_at: context.now.toISOString(),
@@ -225,6 +227,7 @@ export function buildAgentSessionReport(options) {
225
227
  const uploadOutcomes = sessionUploadOutcomesByKey(options.outcomes);
226
228
  return [...bestByKey.values()].map((result) => sessionReportEntry(result, {
227
229
  ...uploadOutcomes,
230
+ ticketByWorktree: options.ticketByWorktree,
228
231
  claudePriorDurablePointers: options.claudePriorDurablePointers,
229
232
  now: options.now,
230
233
  }));
@@ -1,15 +1,15 @@
1
1
  import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
2
2
  export function parseUsageArgs(args) {
3
- const values = parseNamedArgs(args, { allowedFlags: ["--by-subject", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--subject", "--repo", "--session", "--from-window", "--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
4
- if (["--by-repo", "--by-topic", "--by-subject"].filter(flag => values.booleans.has(flag)).length > 1)
5
- throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject.");
3
+ const values = parseNamedArgs(args, { allowedFlags: ["--by-task", "--task", "--by-subject", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--task", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
4
+ if (["--by-task", "--by-repo", "--by-topic", "--by-subject"].filter(flag => values.booleans.has(flag)).length > 1)
5
+ throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject or --by-task.");
6
6
  const minConfidence = values.flags.get("--min-confidence");
7
7
  if (minConfidence !== undefined && !["high", "medium", "low"].includes(minConfidence))
8
8
  throw new Error("--min-confidence accepts high, medium or low.");
9
9
  const action = values.positionals[0] ?? "people";
10
10
  if (!["people", "sessions"].includes(action) || values.positionals.length > 1)
11
11
  throw new Error("usage takes people or sessions.");
12
- if (action === "sessions" && !values.flags.get("--subject") && !values.flags.get("--session"))
13
- throw new Error("usage sessions requires --subject or --session.");
14
- return { bySubject: values.booleans.has("--by-subject"), subject: optionalNonEmpty(values.flags.get("--subject")), repo: optionalNonEmpty(values.flags.get("--repo")), session: optionalNonEmpty(values.flags.get("--session")), fromWindow: optionalNonEmpty(values.flags.get("--from-window")), kind: "usage", ...(minConfidence ? { minConfidence: minConfidence } : {}), byTopic: values.booleans.has("--by-topic"), byRepo: values.booleans.has("--by-repo"), person: optionalNonEmpty(values.flags.get("--person")), all: values.booleans.has("--all"), action: action, since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
12
+ if (action === "sessions" && !values.flags.get("--subject") && !values.flags.get("--session") && !values.flags.get("--task"))
13
+ throw new Error("usage sessions requires --task, --subject or --session.");
14
+ return { byTask: values.booleans.has("--by-task"), task: optionalNonEmpty(values.flags.get("--task")), bySubject: values.booleans.has("--by-subject"), subject: optionalNonEmpty(values.flags.get("--subject")), repo: optionalNonEmpty(values.flags.get("--repo")), session: optionalNonEmpty(values.flags.get("--session")), fromWindow: optionalNonEmpty(values.flags.get("--from-window")), kind: "usage", ...(minConfidence ? { minConfidence: minConfidence } : {}), byTopic: values.booleans.has("--by-topic"), byRepo: values.booleans.has("--by-repo"), person: optionalNonEmpty(values.flags.get("--person")), all: values.booleans.has("--all"), action: action, since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
15
15
  }
@@ -143,8 +143,8 @@ export const TOWER_COMMAND_HELP = [
143
143
  [
144
144
  "usage",
145
145
  [
146
- "Usage: cockpit usage people [--by-repo | --by-topic | --by-subject] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
147
- " cockpit usage sessions --subject <name> [--repo <label>] [--since <window>] [--json]",
146
+ "Usage: cockpit usage people [--by-repo | --by-topic | --by-subject | --by-task] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
147
+ " cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
148
148
  " cockpit usage sessions --session <id> [--from-window <n>] [--json]",
149
149
  " --by-topic groups work types; --by-subject groups open subjects from session summaries.",
150
150
  "",
@@ -104,10 +104,10 @@ export function localCommandHelp(command) {
104
104
  ` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
105
105
  " cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
106
106
  " cockpit careers [list|show <id>|rescreen <id>] [--role <slug>] [--min-score <n>] [--since <date>] [--json]",
107
- " cockpit usage sessions --subject <name> [--repo <label>] [--since <window>] [--json]",
107
+ " cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
108
108
  " cockpit usage sessions --session <id> [--from-window <n>] [--json]",
109
109
  " --by-topic groups work types; --by-subject groups open subjects from session summaries.",
110
- " cockpit usage people [--by-repo | --by-topic | --by-subject] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--detail] [--since <n>d|<n>h|<iso>] [--until <iso>] [--include-automated] [--dashboard-url <url>] [--json]",
110
+ " cockpit usage people [--by-repo | --by-topic | --by-subject | --by-task] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--detail] [--since <n>d|<n>h|<iso>] [--until <iso>] [--include-automated] [--dashboard-url <url>] [--json]",
111
111
  "",
112
112
  `Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
113
113
  ].join("\n");
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.111");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.112");
19
19
  return 0;
20
20
  }
21
21
 
@@ -1,3 +1,4 @@
1
+ import { readLocalWorkContextForRepo } from "../local-state.js";
1
2
  /**
2
3
  * One attributed sync pass: scan both session stores, attribute what they hold,
3
4
  * upload it through every target worktree, write down what happened, and judge
@@ -91,7 +92,16 @@ export async function runAttributedWorktreeSync(options) {
91
92
  plan,
92
93
  scan,
93
94
  });
95
+ const ticketByWorktree = new Map();
96
+ for (const worktree of options.worktrees) {
97
+ const context = await readLocalWorkContextForRepo(paths, worktree.repo_root).catch(() => null);
98
+ const ticket = context?.linear_ticket_id ?? context?.active_ticket_id;
99
+ if (ticket)
100
+ ticketByWorktree.set(worktree.worktree_fingerprint, ticket);
101
+ }
102
+ console.error("[session-sync] bindings read", JSON.stringify({ bound_worktrees: ticketByWorktree.size, reason: "active_work_context" }));
94
103
  const sessions = buildAgentSessionReport({
104
+ ticketByWorktree,
95
105
  codexResults: scan.codexAttribution.results,
96
106
  claudeResults: scan.claudeAttribution.results,
97
107
  outcomes: worktreePass.outcomes,
@@ -5,6 +5,8 @@ import { writeLine } from "./cli-io.js";
5
5
  export async function runUsage(command, io) {
6
6
  const door = await openAgentDoor("usage", command, io);
7
7
  const query = new URLSearchParams({ since: command.since });
8
+ if (command.byTask)
9
+ query.set("groupBy", "ticket");
8
10
  if (command.bySubject)
9
11
  query.set("groupBy", "topic");
10
12
  if (command.minConfidence)
@@ -18,7 +20,7 @@ export async function runUsage(command, io) {
18
20
  if (command.includeAutomated)
19
21
  query.set("include_automated", "1");
20
22
  if (command.action === "sessions") {
21
- for (const [key, value] of [["subject", command.subject], ["repo", command.repo], ["session", command.session], ["fromWindow", command.fromWindow]])
23
+ for (const [key, value] of [["task", command.task], ["subject", command.subject], ["repo", command.repo], ["session", command.session], ["fromWindow", command.fromWindow]])
22
24
  if (value)
23
25
  query.set(key, value);
24
26
  const response = await askAgentDoor(door, { path: `/api/usage/sessions?${query}`, method: "GET", label: "usage sessions", timeoutMs: 30_000 });
@@ -50,12 +52,14 @@ export async function runUsage(command, io) {
50
52
  return failAgentDoor(door, "[usage]", "topic_grouping_unavailable", "The dashboard returned person totals without topic rows. Deploy the dashboard topic split before using --by-topic.");
51
53
  if (command.bySubject && body.people?.some(person => !Array.isArray(person.subjects)))
52
54
  return failAgentDoor(door, "[usage]", "subject_grouping_unavailable", "Deploy the dashboard subject split before using --by-subject.");
55
+ if (command.byTask && body.people?.some(person => !Array.isArray(person.tickets)))
56
+ return failAgentDoor(door, "[usage]", "ticket_grouping_unavailable", "Deploy the dashboard task split before using --by-task.");
53
57
  if (door.json)
54
58
  return emitAgentDoor(door, body);
55
59
  let nameWidth = 20;
56
- if (command.byRepo || command.byTopic) {
60
+ if (command.byTask || command.byRepo || command.byTopic) {
57
61
  for (const person of body.people ?? []) {
58
- const repos = (command.byTopic ? person.task_types : person.repos) ?? [];
62
+ const repos = (command.byTask ? person.tickets : command.byTopic ? person.task_types : person.repos) ?? [];
59
63
  for (const repo of command.all ? repos : repos.slice(0, 10))
60
64
  nameWidth = Math.max(nameWidth, repo.repo_label.length + 2);
61
65
  }
@@ -84,15 +88,17 @@ export async function runUsage(command, io) {
84
88
  for (const sub of subject.children ?? [])
85
89
  printRow([` ${sub.subtopic}`, formatCount(sub.tokens), formatUsageDollars(sub.api_list_price_equivalent_usd), `${sub.extracted_sessions}/${sub.sessions}`, ...(command.detail ? [sub.output, sub.input, sub.cache_read, sub.cache_creation].map(formatCount) : [])]);
86
90
  }
87
- if (command.byRepo || command.byTopic) {
88
- const repos = (command.byTopic ? row.task_types : row.repos) ?? [];
91
+ if (command.byTask || command.byRepo || command.byTopic) {
92
+ const repos = (command.byTask ? row.tickets : command.byTopic ? row.task_types : row.repos) ?? [];
89
93
  for (const repo of command.all ? repos : repos.slice(0, 10)) {
94
+ if (command.byTask)
95
+ writeLine(io.stdout, ` ${repo.ticket_title ?? ""} Sources: ${Object.entries(repo.binding_sources ?? {}).map(([source, count]) => `${source} ${count}`).join(", ")}`);
90
96
  if (command.byTopic && repo.confidence)
91
97
  writeLine(io.stdout, ` Confidence: ${repo.confidence.high} high, ${repo.confidence.medium} medium, ${repo.confidence.low} low`);
92
98
  printRow([` ${repo.repo_label}`, formatCount(repo.tokens), formatUsageDollars(repo.api_list_price_equivalent_usd), `${repo.extracted_sessions}/${repo.sessions}`, ...(command.detail ? [repo.output, repo.input, repo.cache_read, repo.cache_creation].map(formatCount) : [])]);
93
99
  }
94
100
  if (!command.all && repos.length > 10)
95
- writeLine(io.stdout, ` and ${repos.length - 10} more ${command.byTopic ? "topics" : "repos"}`);
101
+ writeLine(io.stdout, ` and ${repos.length - 10} more ${command.byTask ? "tasks" : command.byTopic ? "topics" : "repos"}`);
96
102
  }
97
103
  }
98
104
  writeLine(io.stdout, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.111",
3
+ "version": "0.2.112",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,8 +27,8 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.29",
31
- "@bli-cockpit/mcp": "0.1.39",
32
- "@bli-cockpit/telemetry-core": "0.1.45"
30
+ "@bli-cockpit/memory-mcp": "0.1.30",
31
+ "@bli-cockpit/mcp": "0.1.40",
32
+ "@bli-cockpit/telemetry-core": "0.1.46"
33
33
  }
34
34
  }