@bli-cockpit/cli 0.2.63 → 0.2.64

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.
@@ -25,6 +25,7 @@
25
25
  * (`agent-door.ts`) — `issue_not_found_or_unreadable`, `invalid_state`,
26
26
  * `needs_rls_client`, `comment_too_long`, and so on.
27
27
  */
28
+ import { mirroredTicketIdInTitle } from "@bli-cockpit/telemetry-core";
28
29
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
29
30
  import { writeLine } from "./cli-io.js";
30
31
  import { READ_DEADLINE_MS, TAG, resolveProjectId, } from "./issue-contracts.js";
@@ -118,17 +119,25 @@ async function showIssue(command, door) {
118
119
  if (!commentsAnswer.ok) {
119
120
  writeLine(door.io.stderr, `${TAG} comments unread ${JSON.stringify({ reason: commentsAnswer.reason, issue_id: issue.id })}`);
120
121
  }
122
+ // BLI-3779: during the dual-run a Tower-native row stands for a Linear
123
+ // ticket, and says so in its own title (`… [BLI-3779]`). Read it out rather
124
+ // than making every caller parse the title — `cockpit start --ticket` binds
125
+ // a session to both ids, and this is where a person checks which they are.
126
+ const linearTicketId = mirroredTicketIdInTitle(issue.title ?? "");
121
127
  if (door.json) {
122
128
  return emitAgentDoor(door, {
123
129
  ok: true,
124
130
  issue,
125
131
  comments,
132
+ ...(linearTicketId ? { linear_ticket_id: linearTicketId } : {}),
126
133
  ...(commentsAnswer.ok ? {} : { comments_unread_reason: commentsAnswer.reason }),
127
134
  });
128
135
  }
129
136
  writeLine(door.io.stdout, `${issue.identifier} ${issue.title}`);
130
137
  writeLine(door.io.stdout, `${issue.state} · priority ${issue.priority ?? 0} · assignee ${issue.assignee_id ?? "(nobody)"} · updated ${issue.updated_at}`);
131
138
  writeLine(door.io.stdout, `id ${issue.id}${issue.parent_id ? ` · parent ${issue.parent_id}` : ""}`);
139
+ if (linearTicketId)
140
+ writeLine(door.io.stdout, `Linear: ${linearTicketId}`);
132
141
  writeLine(door.io.stdout, "");
133
142
  writeLine(door.io.stdout, issue.description ?? "(no description)");
134
143
  if (comments.length > 0) {
@@ -195,6 +195,10 @@ export function localSubcommandHelp(command) {
195
195
  "",
196
196
  "Starts collecting your work in the background. If you point it at a parent folder it covers every repo inside.",
197
197
  "Add --ticket only when the work already has a visible ticket; omit it to preserve an existing binding.",
198
+ "--ticket takes EITHER tracker's id while both are running: a Tower issue (BLI-10000 and up, `cockpit issue list`)",
199
+ "or a Linear ticket. Tower is asked which row the id names, and the context records both ids when a row proves the pair —",
200
+ "a Tower-native issue whose title ends in `[BLI-3779]` mirrors that Linear ticket. An id Tower cannot place still binds:",
201
+ "the reason is printed, and the session is never held up by a tracker lookup.",
198
202
  "Use --clear-ticket to go back to collecting general work with no ticket attached.",
199
203
  "Use --topic/--intent/--phase for planning, discovery, and learning work that has no ticket yet.",
200
204
  "Supported intents: implementation, bug_fix, root_cause_analysis, planning, discovery, review, testing, documentation, release, learning, coordination, maintenance, analysis, unknown, other.",
@@ -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.63");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.64");
19
19
  return 0;
20
20
  }
21
21
 
@@ -0,0 +1,233 @@
1
+ /**
2
+ * WHICH TICKET IS THIS, IN BOTH TRACKERS (BLI-3779).
3
+ *
4
+ * Tower runs its own issue tracker beside Linear during the dual-run, and one
5
+ * piece of work has an id in each: a Tower-native row (`BLI-10019`) whose
6
+ * title ends with the Linear id it mirrors (`… [BLI-3779]`). Before this
7
+ * module `cockpit start --ticket` recorded the string a person typed and
8
+ * nothing else, so a session bound to `BLI-3779` was invisible to every reader
9
+ * that keys on `work_issues`, and one bound to a Tower-native id resolved to
10
+ * nothing at all.
11
+ *
12
+ * This asks TOWER — through `GET /api/work/issues/<id>` and its `mirrors=`
13
+ * filter, the same doors `cockpit issue` uses — and hands `start.ts` both ids
14
+ * to record beside the one that was typed.
15
+ *
16
+ * Three rules it does not break:
17
+ *
18
+ * - **Nothing blocks the session.** The session-first commandment applies to
19
+ * a ticket lookup exactly as it does to attribution: an unpaired machine,
20
+ * an unreachable Tower, a typo, an id no tracker has — every one of them
21
+ * returns a NAMED outcome and `cockpit start` binds the context anyway.
22
+ * This module never throws.
23
+ * - **Only what a row proved is recorded.** A Tower id comes from a row that
24
+ * was read; a Linear id comes from that row's title or from the imported
25
+ * row's own identifier. Nothing is inferred from the number alone, and two
26
+ * Tower rows mirroring one Linear id record NEITHER — `ambiguous` is the
27
+ * `person-identity.ts` answer, and guessing binds a session to the wrong
28
+ * ticket.
29
+ * - **`active_ticket_id` stays exactly what was typed.** Rewriting it to the
30
+ * resolved id would move every downstream attribution — the ambient
31
+ * envelope, the ladder's per-ticket rung — onto an id nobody bound.
32
+ *
33
+ * One deployment hazard is handled on purpose: a laptop on a NEW CLI may talk
34
+ * to an OLDER dashboard that does not know the `mirrors=` filter and would
35
+ * answer the unfiltered list. Every returned row is therefore re-checked
36
+ * against the title convention here, and the request carries a small `limit`,
37
+ * so an old server can never make this bind to a row that mirrors something
38
+ * else. What it CAN cost is completeness, and the live probe on 2026-09-05
39
+ * showed both halves: against production-before-this-route the page happened
40
+ * to contain the right mirror (bound correctly), while a SECOND mirror of the
41
+ * same ticket sat outside the page and the ambiguity went unseen. Once the
42
+ * door ships, the filter is the server's and the answer is the whole set.
43
+ */
44
+ import { isTowerNativeIssueId, mirroredTicketIdInTitle, normalizeIssueIdentifier, } from "@bli-cockpit/telemetry-core";
45
+ import { askAgentDoor, openAgentDoor } from "./agent-door.js";
46
+ import { writeLine } from "./cli-io.js";
47
+ const TAG = "[start ticket]";
48
+ /** `cockpit start` must not stall on a tracker: two short reads at most. */
49
+ export const TICKET_LOOKUP_DEADLINE_MS = 8_000;
50
+ /** How many mirror candidates are worth naming before the answer is "several". */
51
+ const MIRROR_CANDIDATE_LIMIT = 5;
52
+ /**
53
+ * Asks Tower which issue a `--ticket` value names (named for the question it
54
+ * asks, not for the field it fills — `local-state-work-context.ts` has its own
55
+ * private `resolveTicketBinding`, which decides bind-vs-clear-vs-carry).
56
+ * Never throws; the caller binds the work context whatever comes back.
57
+ */
58
+ export async function lookUpTicketInTower(ticketId, options) {
59
+ const identifier = normalizeIssueIdentifier(ticketId);
60
+ if (!identifier) {
61
+ return log({
62
+ ticket_id: ticketId,
63
+ status: "not_checked",
64
+ reason: "not_an_issue_identifier",
65
+ detail: `"${ticketId}" is not an identifier like BLI-3779, so no tracker was asked. The work context is bound to it as typed.`,
66
+ }, options.io);
67
+ }
68
+ let door;
69
+ try {
70
+ door = await openAgentDoor("start", { homeDir: options.homeDir, dashboardUrl: options.dashboardUrl, json: true }, options.io);
71
+ }
72
+ catch {
73
+ return log({
74
+ ticket_id: ticketId,
75
+ status: "not_checked",
76
+ reason: "collector_not_paired",
77
+ detail: `This machine has no Tower session, so ${identifier} was not looked up. Run \`cockpit login\` to bind tickets to Tower issues; the work context is bound either way.`,
78
+ }, options.io);
79
+ }
80
+ try {
81
+ const binding = isTowerNativeIssueId(identifier)
82
+ ? await resolveTowerNative(door, ticketId, identifier)
83
+ : await resolveLinearShaped(door, ticketId, identifier);
84
+ return log(binding, options.io);
85
+ }
86
+ catch (error) {
87
+ // A lookup must never take the session down with it.
88
+ return log({
89
+ ticket_id: ticketId,
90
+ status: "not_checked",
91
+ reason: "tower_unreachable",
92
+ detail: `Tower could not be asked about ${identifier} (${error instanceof Error ? error.name : "unknown error"}). The work context is bound to it as typed.`,
93
+ }, options.io);
94
+ }
95
+ }
96
+ /**
97
+ * An id at or above the Tower floor: only Tower's own tracker could have
98
+ * minted it, so one read decides it and Linear is not in the question.
99
+ */
100
+ async function resolveTowerNative(door, ticketId, identifier) {
101
+ const found = await readIssue(door, identifier);
102
+ if (found.status === "failed")
103
+ return doorRefusal(ticketId, identifier, found.reason);
104
+ if (found.status === "missing") {
105
+ return {
106
+ ticket_id: ticketId,
107
+ status: "unresolved",
108
+ reason: "ticket_not_found_in_tower",
109
+ detail: `Tower has no issue ${identifier} (or you cannot read it). That number is in the range Tower mints, so Linear cannot hold it either. The work context is bound to it as typed.`,
110
+ };
111
+ }
112
+ const linear = mirroredTicketIdInTitle(found.issue.title);
113
+ return {
114
+ ticket_id: ticketId,
115
+ tower_issue_id: found.issue.identifier,
116
+ ...(linear ? { linear_ticket_id: linear } : {}),
117
+ status: linear ? "tower_mirrors_linear" : "tower_native",
118
+ detail: linear
119
+ ? `Bound to Tower issue ${found.issue.identifier}, which mirrors ${linear}.`
120
+ : `Bound to Tower issue ${found.issue.identifier}.`,
121
+ };
122
+ }
123
+ /**
124
+ * An id below the Tower floor belongs to Linear's range. Two reads, in this
125
+ * order:
126
+ *
127
+ * 1. the Tower-NATIVE row that mirrors it (`… [BLI-3779]`) — the row a person
128
+ * actually works in during the dual-run, per Edward's 2026-09-05 order;
129
+ * 2. failing that, a row carrying the identifier itself, which is what the
130
+ * Linear import writes (`work_issues.source = 'linear'`).
131
+ */
132
+ async function resolveLinearShaped(door, ticketId, identifier) {
133
+ const mirrors = await readMirrors(door, identifier);
134
+ if (mirrors.status === "failed")
135
+ return doorRefusal(ticketId, identifier, mirrors.reason);
136
+ if (mirrors.issues.length === 1) {
137
+ const mirror = mirrors.issues[0];
138
+ return {
139
+ ticket_id: ticketId,
140
+ tower_issue_id: mirror.identifier,
141
+ linear_ticket_id: identifier,
142
+ status: "tower_mirrors_linear",
143
+ detail: `Bound to ${identifier} and to Tower issue ${mirror.identifier}, which mirrors it.`,
144
+ };
145
+ }
146
+ if (mirrors.issues.length > 1) {
147
+ const candidates = mirrors.issues.map((issue) => issue.identifier);
148
+ return {
149
+ ticket_id: ticketId,
150
+ linear_ticket_id: identifier,
151
+ status: "linear_only",
152
+ reason: "ambiguous_tower_mirror",
153
+ candidates,
154
+ detail: `${candidates.length} Tower issues mirror ${identifier} (${candidates.join(", ")}), so none was chosen — pass the one you mean to \`cockpit start --ticket\`. The context is bound to ${identifier}.`,
155
+ };
156
+ }
157
+ const imported = await readIssue(door, identifier);
158
+ if (imported.status === "failed")
159
+ return doorRefusal(ticketId, identifier, imported.reason);
160
+ if (imported.status === "found") {
161
+ return {
162
+ ticket_id: ticketId,
163
+ tower_issue_id: imported.issue.identifier,
164
+ linear_ticket_id: identifier,
165
+ status: "tower_mirrors_linear",
166
+ detail: `Bound to ${identifier}, which Tower holds under the same identifier.`,
167
+ };
168
+ }
169
+ return {
170
+ ticket_id: ticketId,
171
+ status: "unresolved",
172
+ reason: "ticket_not_found_in_linear",
173
+ detail: `Tower has no row for ${identifier} and no Tower issue mirrors it — Linear itself was not asked, so this says the mirror is missing, not that the ticket is. The work context is bound to it as typed.`,
174
+ };
175
+ }
176
+ function doorRefusal(ticketId, identifier, reason) {
177
+ return {
178
+ ticket_id: ticketId,
179
+ status: "not_checked",
180
+ reason: "tower_unreachable",
181
+ door_reason: reason,
182
+ detail: `Tower could not answer about ${identifier} (${reason}). The work context is bound to it as typed.`,
183
+ };
184
+ }
185
+ /** One issue by identifier. A 404 is an ANSWER, not a failure. */
186
+ async function readIssue(door, identifier) {
187
+ const answer = await askAgentDoor(door, {
188
+ path: `/api/work/issues/${encodeURIComponent(identifier)}`,
189
+ method: "GET",
190
+ label: "start ticket read",
191
+ timeoutMs: TICKET_LOOKUP_DEADLINE_MS,
192
+ });
193
+ if (!answer.ok) {
194
+ if (answer.httpStatus === 404)
195
+ return { status: "missing" };
196
+ return { status: "failed", reason: answer.reason };
197
+ }
198
+ const issue = answer.body.issue;
199
+ return issue ? { status: "found", issue } : { status: "missing" };
200
+ }
201
+ /**
202
+ * The Tower rows whose title carries `[<identifier>]`. The server filters, and
203
+ * the answer is checked against the same convention here — an older dashboard
204
+ * that ignores `mirrors=` returns an unfiltered page, and accepting it would
205
+ * bind the session to a row that mirrors nothing.
206
+ */
207
+ async function readMirrors(door, identifier) {
208
+ const answer = await askAgentDoor(door, {
209
+ path: `/api/work/issues?mirrors=${encodeURIComponent(identifier)}&limit=${MIRROR_CANDIDATE_LIMIT}`,
210
+ method: "GET",
211
+ label: "start ticket mirrors",
212
+ timeoutMs: TICKET_LOOKUP_DEADLINE_MS,
213
+ });
214
+ if (!answer.ok)
215
+ return { status: "failed", reason: answer.reason };
216
+ const issues = answer.body.issues ?? [];
217
+ return {
218
+ status: "ok",
219
+ issues: issues.filter((issue) => mirroredTicketIdInTitle(issue.title ?? "") === identifier),
220
+ };
221
+ }
222
+ /** Every outcome, success included — ids and labels only, never a title. */
223
+ function log(binding, io) {
224
+ writeLine(io.stderr, `${TAG} resolved ${JSON.stringify({
225
+ status: binding.status,
226
+ reason: binding.reason ?? null,
227
+ door_reason: binding.door_reason ?? null,
228
+ tower_issue_id: binding.tower_issue_id ?? null,
229
+ linear_ticket_id: binding.linear_ticket_id ?? null,
230
+ candidates: binding.candidates?.length ?? 0,
231
+ })}`);
232
+ return binding;
233
+ }
@@ -1,8 +1,16 @@
1
1
  import { writeLine } from "./cli-io.js";
2
2
  import { displayTicketId } from "./collection-report.js";
3
3
  import { discoverCommandWorktrees } from "./local-discovery.js";
4
+ import { lookUpTicketInTower } from "./start-ticket-binding.js";
4
5
  import { startLocalWorkContext } from "../local-state.js";
5
6
  export async function runStart(command, io) {
7
+ const binding = command.activeTicketId
8
+ ? await lookUpTicketInTower(command.activeTicketId, { homeDir: command.homeDir, io })
9
+ : null;
10
+ const ticketIds = {
11
+ towerIssueId: binding?.tower_issue_id,
12
+ linearTicketId: binding?.linear_ticket_id,
13
+ };
6
14
  const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos, homeDir: command.homeDir }, io);
7
15
  if (worktrees.length > 1) {
8
16
  const contexts = await Promise.all(worktrees.map((worktree) => startLocalWorkContext({
@@ -10,6 +18,7 @@ export async function runStart(command, io) {
10
18
  repoRoot: worktree.repo_root,
11
19
  branch: command.branch,
12
20
  activeTicketId: command.activeTicketId,
21
+ ...ticketIds,
13
22
  clearTicket: command.clearTicket,
14
23
  topicLabel: command.topicLabel,
15
24
  topicSummaryRedacted: command.topicSummaryRedacted,
@@ -21,27 +30,47 @@ export async function runStart(command, io) {
21
30
  sessionId: command.sessionId,
22
31
  })));
23
32
  if (command.json) {
24
- writeLine(io.stdout, JSON.stringify({ mode: "multi_repo", contexts }, null, 2));
33
+ writeLine(io.stdout, JSON.stringify({ mode: "multi_repo", contexts, ...(binding ? { ticket_binding: binding } : {}) }, null, 2));
25
34
  return 0;
26
35
  }
27
36
  writeLine(io.stdout, `Tower parent work context active for ${contexts.length} worktree(s).`);
28
37
  for (const context of contexts) {
29
38
  writeLine(io.stdout, `- ${context.repo_label ?? context.repo}/${context.worktree_label ?? "worktree"} · ${context.branch} · ${context.work_context_id}`);
30
39
  }
40
+ writeTicketLines(io, binding);
31
41
  return 0;
32
42
  }
33
- const context = await startLocalWorkContext(command);
43
+ const context = await startLocalWorkContext({ ...command, ...ticketIds });
34
44
  if (command.json) {
35
- writeLine(io.stdout, JSON.stringify(context, null, 2));
45
+ writeLine(io.stdout, JSON.stringify({ ...context, ...(binding ? { ticket_binding: binding } : {}) }, null, 2));
36
46
  return 0;
37
47
  }
38
48
  writeLine(io.stdout, "Tower work context active.");
39
49
  writeLine(io.stdout, `Repo: ${context.repo}`);
40
50
  writeLine(io.stdout, `Branch: ${context.branch}`);
41
51
  writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
52
+ writeTicketLines(io, binding, context.tower_issue_id, context.linear_ticket_id);
42
53
  if (context.topic_label || context.work_intent || context.work_phase) {
43
54
  writeLine(io.stdout, `Topic: ${context.topic_label ?? "unlabeled"} · ${context.work_intent ?? "unknown"} · ${context.work_phase ?? "unknown"}`);
44
55
  }
45
56
  writeLine(io.stdout, `Context: ${context.work_context_id}`);
46
57
  return 0;
58
+ }
59
+ /**
60
+ * Both trackers' ids when they are known, and the reason when one is not. A
61
+ * lookup that resolved nothing still prints its sentence: a person who typed
62
+ * a ticket id deserves to know Tower could not place it, and silence there is
63
+ * exactly the "green status hiding a gap" this repo forbids.
64
+ */
65
+ function writeTicketLines(io, binding, towerIssueId, linearTicketId) {
66
+ if (!binding)
67
+ return;
68
+ const tower = towerIssueId ?? binding.tower_issue_id;
69
+ const linear = linearTicketId ?? binding.linear_ticket_id;
70
+ if (tower)
71
+ writeLine(io.stdout, `Tower issue: ${tower}`);
72
+ if (linear)
73
+ writeLine(io.stdout, `Linear: ${linear}`);
74
+ if (!tower || !linear)
75
+ writeLine(io.stdout, binding.detail);
47
76
  }
@@ -66,6 +66,12 @@ export async function runStatus(command, io) {
66
66
  writeLine(io.stdout, `Repo: ${status.repo}`);
67
67
  writeLine(io.stdout, `Branch: ${status.branch}`);
68
68
  writeLine(io.stdout, `Ticket: ${displayTicketId(status.active_ticket_id)}`);
69
+ // BLI-3779: one ticket, two trackers. Each line appears only when a row
70
+ // proved it — an absent line means "not resolved", never "does not exist".
71
+ if (status.tower_issue_id)
72
+ writeLine(io.stdout, `Tower issue: ${status.tower_issue_id}`);
73
+ if (status.linear_ticket_id)
74
+ writeLine(io.stdout, `Linear: ${status.linear_ticket_id}`);
69
75
  writeLine(io.stdout, `Work: ${displayWorkLabel(status)}`);
70
76
  writeLine(io.stdout, `Last collected: ${lastCollectedLine(status.collector_freshness)}`);
71
77
  writeLine(io.stdout, `Version: ${status.collector_version}`);
@@ -30,6 +30,8 @@ export async function inspectLocalCollectorStatus(options = {}) {
30
30
  repo: context?.repo ?? identity.repo_root,
31
31
  branch,
32
32
  active_ticket_id: context?.active_ticket_id ?? null,
33
+ tower_issue_id: context?.tower_issue_id ?? null,
34
+ linear_ticket_id: context?.linear_ticket_id ?? null,
33
35
  work_label: context ? workDisplayLabel(context) : null,
34
36
  work_id: context?.work_context_id ?? null,
35
37
  work_context_id: context?.work_context_id ?? null,
@@ -73,7 +73,7 @@ async function writeLocalWorkContext(options, attributedIdentity) {
73
73
  */
74
74
  function buildWorkContext(input) {
75
75
  const { options, now, identity, branch, operatorId, sessionId, workContextId, existingContext } = input;
76
- const { activeTicketId, ticketBindingCandidates } = resolveTicketBinding(options, existingContext);
76
+ const { activeTicketId, towerIssueId, linearTicketId, ticketBindingCandidates } = resolveTicketBinding(options, existingContext);
77
77
  return LocalWorkContextSchema.parse({
78
78
  work_context_id: workContextId,
79
79
  repo: identity.repo_root,
@@ -90,6 +90,8 @@ function buildWorkContext(input) {
90
90
  started_at: existingContext?.started_at ?? now.toISOString(),
91
91
  updated_at: now.toISOString(),
92
92
  active_ticket_id: activeTicketId,
93
+ tower_issue_id: towerIssueId,
94
+ linear_ticket_id: linearTicketId,
93
95
  ticket_binding_candidates: ticketBindingCandidates,
94
96
  topic_label: options.topicLabel,
95
97
  topic_summary_redacted: options.topicSummaryRedacted,
@@ -119,6 +121,12 @@ function buildWorkContext(input) {
119
121
  /**
120
122
  * Three answers, not two: a named ticket binds, `--clear-ticket` unbinds, and
121
123
  * saying neither leaves whatever the previous `cockpit start` bound in place.
124
+ *
125
+ * The other tracker's ids (BLI-3779) follow the SAME three answers, and they
126
+ * follow the TYPED ticket rather than each other: naming a ticket replaces
127
+ * them with whatever this run resolved — including with nothing, when the
128
+ * lookup came back unresolved — because carrying the previous ticket's Tower
129
+ * id onto a new one would attribute this session's work to the last ticket.
122
130
  */
123
131
  // The return type is inferred deliberately: naming it would mean indexing the
124
132
  // context schema by field name, and this module keeps its literals to the ones
@@ -127,6 +135,9 @@ function resolveTicketBinding(options, existingContext) {
127
135
  const activeTicketId = options.clearTicket
128
136
  ? undefined
129
137
  : (options.activeTicketId ?? existingContext?.active_ticket_id);
138
+ const rebinding = Boolean(options.clearTicket || options.activeTicketId);
139
+ const towerIssueId = rebinding ? options.towerIssueId : existingContext?.tower_issue_id;
140
+ const linearTicketId = rebinding ? options.linearTicketId : existingContext?.linear_ticket_id;
130
141
  const ticketBindingCandidates = options.activeTicketId
131
142
  ? [
132
143
  {
@@ -139,7 +150,7 @@ function resolveTicketBinding(options, existingContext) {
139
150
  : options.clearTicket
140
151
  ? []
141
152
  : (existingContext?.ticket_binding_candidates ?? []);
142
- return { activeTicketId, ticketBindingCandidates };
153
+ return { activeTicketId, towerIssueId, linearTicketId, ticketBindingCandidates };
143
154
  }
144
155
  /** The active context: what the last `cockpit start` on this machine bound, whatever the folder. */
145
156
  export async function readLocalWorkContext(paths) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.63",
3
+ "version": "0.2.64",
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-runtime-files.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.8",
31
- "@bli-cockpit/mcp": "0.1.6",
32
- "@bli-cockpit/telemetry-core": "0.1.29"
30
+ "@bli-cockpit/memory-mcp": "0.1.9",
31
+ "@bli-cockpit/mcp": "0.1.7",
32
+ "@bli-cockpit/telemetry-core": "0.1.30"
33
33
  }
34
34
  }