@clipboard-health/groundcrew 4.2.3 → 4.3.0

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 CHANGED
@@ -20,28 +20,17 @@
20
20
  $ crew status HRD-446
21
21
  groundcrew status HRD-446
22
22
  ========================
23
- ticket: hrd-446
23
+ ticket: hrd-446 in-progress https://linear.app/example/issue/HRD-446
24
+ title: Add retry logic to the sync job
25
+ run: running; model=claude; updated=2026-05-26T00:01:00.000Z; resumes=0
26
+ workspace: live
24
27
 
25
- Config snapshot
26
- ---------------
27
- projectDir: /dev/workspaces
28
- repositories: owner/repo
29
- git: remote=origin; defaultBranch=main
30
- workspaceKind: auto
31
-
32
- Worktree state
33
- --------------
28
+ Worktrees
29
+ ---------
34
30
  - owner/repo host
35
31
  branch: rocky-hrd-446
32
+ dir: /dev/workspaces/owner/repo-hrd-446
36
33
  git: dirty (2 modified, 1 untracked)
37
-
38
- Workspace probe
39
- ---------------
40
- live: yes
41
-
42
- Last Linear status
43
- ------------------
44
- In Progress (state.type=started) — Add retry logic to the sync job
45
34
  ```
46
35
 
47
36
  ## Why
@@ -217,9 +206,9 @@ Replace `claude` with the sbx agent for the model and `<projectDir>` with `works
217
206
 
218
207
  ## Inspecting status
219
208
 
220
- `crew status <TICKET>` prints a read-only snapshot for one ticket: resolved config, matching worktrees, workspace probe result, recorded run state, recent log lines for that ticket, and the latest Linear status. It does not fetch, recover, tear down, resume, or mutate any local/remote state.
209
+ `crew status <TICKET>` prints a read-only snapshot for one ticket: cached title/URL when present, recorded run state, live workspace presence, matching worktrees, git dirtiness, PR links for matching branches, recent log lines when present, and the ticket status from the configured ticket source. It does not recover, tear down, resume, or mutate any local/remote state.
221
210
 
222
- `crew status` with no ticket prints the current inventory: known worktrees with workspace/run-state presence plus live workspaces reported by the configured backend.
211
+ `crew status` with no ticket prints the current inventory: known worktrees with cached ticket metadata, workspace/run-state agreement, attach hints, worktree paths, PR links, and stray sessions reported by the configured backend. Local worktree/session diagnostics are printed before ticket-source fetches complete; when the source fetch succeeds, status also prints slot usage plus Queue/Blocked sections for eligible Todo tickets. If the source fetch fails, Queue shows `unavailable: <reason>` and the slots line is omitted.
223
212
 
224
213
  Use `crew cleanup <TICKET>` to tear down stale worktrees and `crew resume <TICKET>` to reopen preserved work. Status is intentionally informational only.
225
214
 
@@ -233,26 +222,22 @@ Use `crew cleanup <TICKET>` to tear down stale worktrees and `crew resume <TICKE
233
222
  ```text
234
223
  groundcrew status HRD-442
235
224
  =========================
236
- ticket: hrd-442
225
+ ticket: hrd-442 in-progress https://linear.app/example/issue/HRD-442
226
+ title: Multi-event extractor: year inference can produce date_start > date_end
227
+ run: running; model=claude; updated=2026-05-26T00:01:00.000Z; resumes=0
228
+ workspace: live
237
229
 
238
- Run state
230
+ Worktrees
239
231
  ---------
240
- running; model=claude; updated=2026-05-26T00:01:00.000Z; resumes=0
241
-
242
- Worktree
243
- --------
244
- - herds-social host
232
+ - herds-social/herds host
245
233
  branch: paul-hrd-442
246
- dir: /Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442
234
+ dir: /dev/workspaces/herds-social/herds-hrd-442
247
235
  git: dirty (0 modified, 1 untracked)
236
+ pr: https://github.com/herds-social/herds/pull/224 (open)
248
237
 
249
- Workspace
250
- ---------
251
- live: yes
252
-
253
- Last Linear status
254
- ------------------
255
- In Progress (state.type=started) — Multi-event extractor: year inference can produce date_start > date_end
238
+ Recent logs
239
+ -----------
240
+ [10:15:30] Workspace "hrd-442" launched
256
241
  ```
257
242
 
258
243
  </details>
@@ -463,7 +448,7 @@ op run --env-file .env.1password -- crew doctor
463
448
 
464
449
  ## Troubleshooting
465
450
 
466
- First stop for "what exists locally right now": `crew status <ticket>` shows the ticket's worktrees, workspace presence, run state, logs, and latest Linear status. Use `crew doctor` when you need to verify host setup.
451
+ First stop for "what exists locally right now": `crew status <ticket>` shows the ticket's worktrees, workspace presence, run state, logs, and ticket-source status. Use `crew doctor` when you need to verify host setup.
467
452
 
468
453
  <details>
469
454
  <summary>Safehouse-already-wrapped commands are not re-wrapped</summary>
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAaD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAiNjE;AAUD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAQrE"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAaD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAqNjE;AAUD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAQrE"}
@@ -60,7 +60,11 @@ export function createDispatcher(deps) {
60
60
  repository: issue.repository,
61
61
  ticket: ticketId,
62
62
  model: issue.model,
63
- details: { title: issue.title, description: issue.description },
63
+ details: {
64
+ title: issue.title,
65
+ description: issue.description,
66
+ ...(issue.url === undefined ? {} : { url: issue.url }),
67
+ },
64
68
  };
65
69
  await (signal === undefined
66
70
  ? setupWorkspace(config, setupOptions)
@@ -2,6 +2,8 @@ import { type ResolvedConfig } from "../lib/config.ts";
2
2
  export interface TicketDetails {
3
3
  title: string;
4
4
  description: string;
5
+ /** Direct web URL for the ticket; cached into RunState when present. */
6
+ url?: string;
5
7
  }
6
8
  export interface SetupWorkspaceOptions {
7
9
  ticket: string;
@@ -1 +1 @@
1
- {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBnE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAyGf;AAiHD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAwCf"}
1
+ {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBnE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA6Gf;AAqHD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
@@ -102,6 +102,8 @@ export async function setupWorkspace(config, options, runOptions = {}) {
102
102
  branchName,
103
103
  workspaceName: ticket,
104
104
  state: "running",
105
+ title: ticketDetails.title,
106
+ ...(ticketDetails.url === undefined ? {} : { url: ticketDetails.url }),
105
107
  });
106
108
  log(`Workspace "${ticket}" launched (${model})`);
107
109
  log(` Worktree: ${launchDir}`);
@@ -122,6 +124,8 @@ export async function setupWorkspace(config, options, runOptions = {}) {
122
124
  workspaceName: ticket,
123
125
  state: "failed-to-launch",
124
126
  detail: errorMessage(error),
127
+ title: options.details.title,
128
+ ...(options.details.url === undefined ? {} : { url: options.details.url }),
125
129
  });
126
130
  throw error;
127
131
  }
@@ -167,7 +171,9 @@ function recordRunStateBestEffort(arguments_) {
167
171
  branchName: arguments_.branchName,
168
172
  workspaceName: arguments_.workspaceName,
169
173
  state: arguments_.state,
174
+ title: arguments_.title,
170
175
  ...(arguments_.detail === undefined ? {} : { detail: arguments_.detail }),
176
+ ...(arguments_.url === undefined ? {} : { url: arguments_.url }),
171
177
  },
172
178
  });
173
179
  }
@@ -242,7 +248,11 @@ export async function setupWorkspaceCli(ticket, options = {}) {
242
248
  ticket: naturalId,
243
249
  repository: resolved.repository,
244
250
  model: resolved.model,
245
- details: { title: resolved.title, description: resolved.description },
251
+ details: {
252
+ title: resolved.title,
253
+ description: resolved.description,
254
+ ...(resolved.url === undefined ? {} : { url: resolved.url }),
255
+ },
246
256
  });
247
257
  await board.markInProgress(resolved);
248
258
  }
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyLD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAanE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA6gBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
@@ -1,8 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { getLinearClient } from "../lib/adapters/linear/client.js";
3
- import { fetchRawLinearIssue } from "../lib/adapters/linear/fetch.js";
2
+ import { createBoard } from "../lib/board.js";
3
+ import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
4
4
  import { loadConfig } from "../lib/config.js";
5
+ import { findPullRequestsForBranch } from "../lib/pullRequests.js";
5
6
  import { readRunState } from "../lib/runState.js";
7
+ import { isGroundcrewIssue, naturalIdFromCanonical, } from "../lib/ticketSource.js";
6
8
  import { errorMessage, withLogOutputSuppressed, writeOutput } from "../lib/util.js";
7
9
  import { workspaces } from "../lib/workspaces.js";
8
10
  import { worktrees } from "../lib/worktrees.js";
@@ -25,16 +27,6 @@ function writeSection(title) {
25
27
  writeOutput(title);
26
28
  writeOutput("-".repeat(title.length));
27
29
  }
28
- function writeConfigSnapshot(config) {
29
- writeSection("Config snapshot");
30
- writeOutput(`projectDir: ${config.workspace.projectDir}`);
31
- writeOutput(`repositories: ${config.workspace.knownRepositories.join(", ")}`);
32
- writeOutput(`git: remote=${config.git.remote}; defaultBranch=${config.git.defaultBranch}`);
33
- writeOutput(`workspaceKind: ${config.workspaceKind}`);
34
- writeOutput(`local.runner: ${config.local.runner}`);
35
- writeOutput(`models: default=${config.models.default}; enabled=${Object.keys(config.models.definitions).join(", ")}`);
36
- writeOutput(`logFile: ${config.logging.file}`);
37
- }
38
30
  function formatDirtiness(dirtiness) {
39
31
  if (dirtiness.kind === "dirty") {
40
32
  return `dirty (${dirtiness.modified} modified, ${dirtiness.untracked} untracked)`;
@@ -42,7 +34,7 @@ function formatDirtiness(dirtiness) {
42
34
  return dirtiness.kind;
43
35
  }
44
36
  async function writeTicketWorktrees(config, ticket) {
45
- writeSection("Worktree state");
37
+ writeSection("Worktrees");
46
38
  const entries = worktrees.findByTicket(config, ticket);
47
39
  if (entries.length === 0) {
48
40
  writeOutput("(none)");
@@ -50,12 +42,21 @@ async function writeTicketWorktrees(config, ticket) {
50
42
  }
51
43
  for (const entry of entries) {
52
44
  // oxlint-disable-next-line no-await-in-loop -- status output is easier to read in worktree order.
53
- const dirtiness = await worktrees.probeWorkingTree({ worktreeDir: entry.dir });
45
+ const dirtiness = await worktrees.probeWorkingTree({
46
+ worktreeDir: entry.dir,
47
+ });
48
+ // oxlint-disable-next-line no-await-in-loop -- one gh lookup per worktree is acceptable; multi-worktree-per-ticket is rare.
49
+ const prs = await findPullRequestsForBranch({
50
+ repository: entry.repository,
51
+ branchName: entry.branchName,
52
+ });
54
53
  writeOutput(`- ${entry.repository} ${entry.kind}`);
55
- writeOutput(` ticket: ${entry.ticket}`);
56
54
  writeOutput(` branch: ${entry.branchName}`);
57
55
  writeOutput(` dir: ${entry.dir}`);
58
56
  writeOutput(` git: ${formatDirtiness(dirtiness)}`);
57
+ if (prs.length > 0) {
58
+ writeOutput(` pr: ${formatPullRequests(prs)}`);
59
+ }
59
60
  }
60
61
  }
61
62
  function workspaceProbeUnavailableLine(probe) {
@@ -63,13 +64,11 @@ function workspaceProbeUnavailableLine(probe) {
63
64
  ? "Workspace probe unavailable"
64
65
  : `Workspace probe unavailable: ${errorMessage(probe.error)}`;
65
66
  }
66
- function writeTicketWorkspace(probe, ticket) {
67
- writeSection("Workspace probe");
67
+ function ticketWorkspaceText(probe, ticket) {
68
68
  if (probe.kind === "unavailable") {
69
- writeOutput(workspaceProbeUnavailableLine(probe));
70
- return;
69
+ return workspaceProbeUnavailableLine(probe);
71
70
  }
72
- writeOutput(`live: ${probe.names.has(ticket) ? "yes" : "no"}`);
71
+ return probe.names.has(ticket) ? "live" : "not live";
73
72
  }
74
73
  function formatRunState(state) {
75
74
  if (state === undefined) {
@@ -93,15 +92,56 @@ function recentTicketLogLines(config, ticket) {
93
92
  .filter((line) => pattern.test(line))
94
93
  .slice(-RECENT_LOG_LINE_COUNT);
95
94
  }
96
- async function linearStatus(ticket) {
95
+ async function resolveTicketSource(config, ticket) {
96
+ const board = await buildBoardForStatus(config);
97
+ return await withLogOutputSuppressed(async () => await board.resolveOne(ticket));
98
+ }
99
+ async function readTicketSourceStatus(config, ticket) {
97
100
  try {
98
- const issue = await fetchRawLinearIssue({ client: getLinearClient(), ticket });
99
- // `stateType` is "" when Linear returned a stateless ticket; surface that
100
- // as "unknown" rather than an empty token.
101
- return `${issue.stateName} (state.type=${issue.stateType || "unknown"}) — ${issue.title}`;
101
+ const issue = await resolveTicketSource(config, ticket);
102
+ if (issue === undefined) {
103
+ return { kind: "not-found" };
104
+ }
105
+ return { kind: "found", issue };
102
106
  }
103
107
  catch (error) {
104
- return `unavailable: ${errorMessage(error)}`;
108
+ return { kind: "unavailable", reason: errorMessage(error) };
109
+ }
110
+ }
111
+ function writeRecentLogs(config, ticket) {
112
+ const logLines = recentTicketLogLines(config, ticket);
113
+ if (logLines.length === 0) {
114
+ return;
115
+ }
116
+ writeSection("Recent logs");
117
+ writeOutput(logLines.join("\n"));
118
+ }
119
+ function formatTicketLine(ticket, runState, sourceStatus) {
120
+ const parts = [`ticket: ${ticket}`];
121
+ if (sourceStatus.kind === "found") {
122
+ parts.push(sourceStatus.issue.status);
123
+ }
124
+ const url = sourceStatus.kind === "found" ? (sourceStatus.issue.url ?? runState?.url) : runState?.url;
125
+ if (url !== undefined) {
126
+ parts.push(url);
127
+ }
128
+ if (sourceStatus.kind === "not-found") {
129
+ parts.push("source not found");
130
+ }
131
+ if (sourceStatus.kind === "unavailable") {
132
+ parts.push(`source unavailable: ${sourceStatus.reason}`);
133
+ }
134
+ return parts.join(" ");
135
+ }
136
+ function writeTicketTitle(runState, sourceStatus) {
137
+ const cachedTitle = runState?.title;
138
+ const sourceTitle = sourceStatus.kind === "found" ? sourceStatus.issue.title : undefined;
139
+ const title = cachedTitle ?? sourceTitle;
140
+ if (title !== undefined) {
141
+ writeOutput(`title: ${title}`);
142
+ }
143
+ if (cachedTitle !== undefined && sourceTitle !== undefined && cachedTitle !== sourceTitle) {
144
+ writeOutput(`source title: ${sourceTitle}`);
105
145
  }
106
146
  }
107
147
  async function writeTicketStatus(config, rawTicket) {
@@ -109,26 +149,118 @@ async function writeTicketStatus(config, rawTicket) {
109
149
  const displayTicket = ticket.toUpperCase();
110
150
  writeOutput(`groundcrew status ${displayTicket}`);
111
151
  writeOutput("=".repeat(`groundcrew status ${displayTicket}`.length));
112
- writeOutput(`ticket: ${ticket}`);
113
- writeConfigSnapshot(config);
152
+ const runState = readRunState(config, ticket);
153
+ const [workspaceProbe, sourceStatus] = await Promise.all([
154
+ withLogOutputSuppressed(async () => await workspaces.probe(config)),
155
+ readTicketSourceStatus(config, ticket),
156
+ ]);
157
+ writeOutput(formatTicketLine(ticket, runState, sourceStatus));
158
+ writeTicketTitle(runState, sourceStatus);
159
+ writeOutput(`run: ${formatRunState(runState)}`);
160
+ writeOutput(`workspace: ${ticketWorkspaceText(workspaceProbe, ticket)}`);
114
161
  await writeTicketWorktrees(config, ticket);
115
- const workspaceProbe = await withLogOutputSuppressed(async () => await workspaces.probe(config));
116
- writeTicketWorkspace(workspaceProbe, ticket);
117
- writeSection("Run state");
118
- writeOutput(formatRunState(readRunState(config, ticket)));
119
- writeSection("Recent logs");
120
- const logLines = recentTicketLogLines(config, ticket);
121
- writeOutput(logLines.length === 0 ? "(none)" : logLines.join("\n"));
122
- writeSection("Last Linear status");
123
- writeOutput(await linearStatus(ticket));
162
+ writeRecentLogs(config, ticket);
163
+ }
164
+ /**
165
+ * Wall-clock elapsed time since the run was first recorded (RunState.createdAt
166
+ * is preserved across resume/interrupt). Returns undefined when the row isn't
167
+ * actively running, when no run state exists, or when the timestamp cannot
168
+ * be parsed.
169
+ */
170
+ function runStateDurationMs(runState, now) {
171
+ if (runState === undefined) {
172
+ return undefined;
173
+ }
174
+ if (runState.state !== "running" && runState.state !== "resumed") {
175
+ return undefined;
176
+ }
177
+ const created = Date.parse(runState.createdAt);
178
+ if (Number.isNaN(created)) {
179
+ return undefined;
180
+ }
181
+ return now.getTime() - created;
182
+ }
183
+ const MS_PER_MINUTE = 60_000;
184
+ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
185
+ const MS_PER_DAY = 24 * MS_PER_HOUR;
186
+ function formatDuration(ms) {
187
+ if (ms < MS_PER_MINUTE) {
188
+ return "<1m";
189
+ }
190
+ if (ms < MS_PER_HOUR) {
191
+ return `${Math.floor(ms / MS_PER_MINUTE)}m`;
192
+ }
193
+ if (ms < MS_PER_DAY) {
194
+ const hours = Math.floor(ms / MS_PER_HOUR);
195
+ const minutes = Math.floor((ms - hours * MS_PER_HOUR) / MS_PER_MINUTE);
196
+ return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
197
+ }
198
+ const days = Math.floor(ms / MS_PER_DAY);
199
+ const hours = Math.floor((ms - days * MS_PER_DAY) / MS_PER_HOUR);
200
+ return hours === 0 ? `${days}d` : `${days}d ${hours}h`;
124
201
  }
125
- function workspacePresence(probe, ticket) {
202
+ /**
203
+ * Combined human-readable state for the inventory row. Surfaces RunState
204
+ * lifecycle and flags the two interesting disagreements with the workspace
205
+ * probe — `(session dead)` when we recorded a running dispatch but no
206
+ * session is alive, and `(stray session)` when a session is alive without
207
+ * any recorded dispatch. `probe.kind === "unavailable"` is treated as
208
+ * "we don't know" and never produces a suffix. When the row is actively
209
+ * running, appends the elapsed wall-clock time since dispatch.
210
+ */
211
+ function inventoryStateText(runState, probe, ticket, now) {
212
+ const lifecycle = runState?.state ?? "idle";
213
+ const duration = runStateDurationMs(runState, now);
214
+ const flags = [];
215
+ if (probe.kind === "ok") {
216
+ const sessionLive = probe.names.has(ticket);
217
+ if (lifecycle === "idle" && sessionLive) {
218
+ flags.push("stray session");
219
+ }
220
+ if ((lifecycle === "running" || lifecycle === "resumed") && !sessionLive) {
221
+ flags.push("session dead");
222
+ }
223
+ }
224
+ if (duration !== undefined) {
225
+ flags.push(formatDuration(duration));
226
+ }
227
+ return flags.length === 0 ? lifecycle : `${lifecycle} (${flags.join(", ")})`;
228
+ }
229
+ /**
230
+ * Hint command for inventory rows where the run-state and the workspace
231
+ * probe disagree. Returned commands are safe defaults; the user is free to
232
+ * ignore them and use `attach:` + `pr:` to investigate first.
233
+ *
234
+ * - Stray session (live session, no run-state record) → `crew cleanup` to
235
+ * tear down the orphaned worktree + close the session.
236
+ * - Session dead (run-state says running/resumed, no live session) →
237
+ * `crew resume` to bring the agent back; the worktree is preserved.
238
+ *
239
+ * No hint when the probe is unavailable (we genuinely don't know whether
240
+ * there's a disagreement) or when the row is healthy.
241
+ */
242
+ function inventoryHint(runState, probe, ticket) {
126
243
  if (probe.kind === "unavailable") {
127
- return "unknown";
244
+ return undefined;
245
+ }
246
+ const lifecycle = runState?.state ?? "idle";
247
+ const sessionLive = probe.names.has(ticket);
248
+ if (lifecycle === "idle" && sessionLive) {
249
+ return `run 'crew cleanup ${ticket}' to clear this stray session`;
128
250
  }
129
- return probe.names.has(ticket) ? "yes" : "no";
251
+ if ((lifecycle === "running" || lifecycle === "resumed") && !sessionLive) {
252
+ return `run 'crew resume ${ticket}' to bring the session back`;
253
+ }
254
+ return undefined;
255
+ }
256
+ const INVENTORY_LABEL_WIDTH = "worktree:".length;
257
+ function inventoryField(label, value) {
258
+ return ` ${`${label}:`.padEnd(INVENTORY_LABEL_WIDTH)} ${value}`;
259
+ }
260
+ function formatPullRequests(prs) {
261
+ return prs.map((pr) => `${pr.url} (${pr.state})`).join(", ");
130
262
  }
131
- function writeInventoryWorktrees(config, probe) {
263
+ async function writeInventoryWorktrees(config, probe) {
132
264
  writeSection("Worktrees");
133
265
  const entries = worktrees
134
266
  .list(config)
@@ -137,31 +269,179 @@ function writeInventoryWorktrees(config, probe) {
137
269
  writeOutput("(none)");
138
270
  return;
139
271
  }
272
+ const accessHints = await collectAccessHints(config, entries);
273
+ const pullRequests = await collectPullRequests(entries);
140
274
  const runStates = new Map();
141
- for (const entry of entries) {
275
+ const now = new Date();
276
+ for (const [index, entry] of entries.entries()) {
142
277
  if (!runStates.has(entry.ticket)) {
143
278
  runStates.set(entry.ticket, readRunState(config, entry.ticket));
144
279
  }
145
280
  const runState = runStates.get(entry.ticket);
146
- writeOutput(`${entry.ticket} ${entry.repository} ${entry.kind} workspace=${workspacePresence(probe, entry.ticket)} run=${runState?.state ?? "none"}`);
147
- writeOutput(` ${entry.branchName} ${entry.dir}`);
281
+ const accessHint = accessHints.get(entry.ticket);
282
+ // `collectPullRequests` guarantees an entry for every (repo, branch)
283
+ // pair seen in `entries`; the lookup always returns the array.
284
+ /* v8 ignore next @preserve -- defensive fallback for a Map key that collectPullRequests always populates */
285
+ const prs = pullRequests.get(pullRequestKey(entry.repository, entry.branchName)) ?? [];
286
+ if (index > 0) {
287
+ writeOutput();
288
+ }
289
+ writeOutput(runState?.url === undefined ? entry.ticket : `${entry.ticket} ${runState.url}`);
290
+ if (runState?.title !== undefined) {
291
+ writeOutput(inventoryField("title", runState.title));
292
+ }
293
+ writeOutput(inventoryField("state", inventoryStateText(runState, probe, entry.ticket, now)));
294
+ writeOutput(inventoryField("repo", entry.repository));
295
+ writeOutput(inventoryField("worktree", entry.dir));
296
+ if (accessHint !== undefined) {
297
+ writeOutput(inventoryField("attach", accessHint.command));
298
+ }
299
+ if (prs.length > 0) {
300
+ writeOutput(inventoryField("pr", formatPullRequests(prs)));
301
+ }
302
+ const hint = inventoryHint(runState, probe, entry.ticket);
303
+ if (hint !== undefined) {
304
+ writeOutput(inventoryField("hint", hint));
305
+ }
148
306
  }
149
307
  }
150
- function writeInventoryWorkspaces(probe) {
151
- writeSection("Live workspaces");
308
+ function pullRequestKey(repository, branchName) {
309
+ return `${repository} ${branchName}`;
310
+ }
311
+ async function collectAccessHints(config, entries) {
312
+ const uniqueTickets = [...new Set(entries.map((entry) => entry.ticket))];
313
+ const results = await Promise.allSettled(uniqueTickets.map(async (ticket) => await workspaces.accessHint(config, ticket)));
314
+ return new Map(uniqueTickets.map((ticket, index) => {
315
+ const result = results[index];
316
+ return [ticket, result?.status === "fulfilled" ? result.value : undefined];
317
+ }));
318
+ }
319
+ async function collectPullRequests(entries) {
320
+ // Same-(repo, branch) entries collapse to one lookup; later inserts
321
+ // overwrite earlier ones with the same identifier, which is fine because
322
+ // gh would return the same PR list for both.
323
+ const uniqueKeys = new Map();
324
+ for (const entry of entries) {
325
+ uniqueKeys.set(pullRequestKey(entry.repository, entry.branchName), {
326
+ repository: entry.repository,
327
+ branchName: entry.branchName,
328
+ });
329
+ }
330
+ const results = await Promise.allSettled([...uniqueKeys.entries()].map(async ([key, { repository, branchName }]) => {
331
+ const prs = await findPullRequestsForBranch({ repository, branchName });
332
+ return [key, prs];
333
+ }));
334
+ return new Map([...uniqueKeys.keys()].map((key, index) => {
335
+ const result = results[index];
336
+ return [key, result?.status === "fulfilled" ? result.value[1] : []];
337
+ }));
338
+ }
339
+ function writeStraySessions(probe, worktreeTickets) {
152
340
  if (probe.kind === "unavailable") {
341
+ // Surface probe failures so the user knows we couldn't classify strays
342
+ // (silently dropping the section would hide that diagnostic).
343
+ writeSection("Stray sessions");
153
344
  writeOutput(workspaceProbeUnavailableLine(probe));
154
345
  return;
155
346
  }
156
- const names = [...probe.names].toSorted();
157
- writeOutput(names.length === 0 ? "(none)" : names.join("\n"));
347
+ const strays = [...probe.names].filter((name) => !worktreeTickets.has(name)).toSorted();
348
+ if (strays.length === 0) {
349
+ return;
350
+ }
351
+ writeSection("Stray sessions");
352
+ writeOutput(strays.join("\n"));
353
+ }
354
+ function isTodoSourceIssue(issue) {
355
+ return issue.status === "todo";
356
+ }
357
+ function hasOpenBlocker(issue) {
358
+ return issue.blockers.some((b) => b.status !== "done");
359
+ }
360
+ function describeOpenBlockers(issue) {
361
+ return issue.blockers
362
+ .filter((b) => b.status !== "done")
363
+ .map((b) => `${naturalIdFromCanonical(b.id)} (${b.nativeStatus ?? b.status})`)
364
+ .join(", ");
365
+ }
366
+ function writeQueueIssue(issue) {
367
+ const naturalId = naturalIdFromCanonical(issue.id);
368
+ writeOutput(issue.url === undefined ? naturalId : `${naturalId} ${issue.url}`);
369
+ writeOutput(inventoryField("title", issue.title));
370
+ writeOutput(inventoryField("repo", issue.repository));
371
+ writeOutput(inventoryField("model", issue.model));
372
+ }
373
+ async function buildBoardForStatus(config) {
374
+ const sources = await buildSources(sourcesFromConfig(config), { globalConfig: config });
375
+ return createBoard(sources);
376
+ }
377
+ /**
378
+ * Single board fetch used by both the slot count header and the
379
+ * Queue/Blocked sections. `sourcesFromConfig` prepends an implicit Linear
380
+ * source when none are configured, so we always attempt; failures
381
+ * (e.g., missing API key) are captured and rendered later as
382
+ * `unavailable: ...` in the Queue section.
383
+ */
384
+ async function fetchBoardForStatus(config) {
385
+ try {
386
+ const board = await buildBoardForStatus(config);
387
+ const { issues } = await withLogOutputSuppressed(async () => await board.fetch());
388
+ return { kind: "ok", issues };
389
+ }
390
+ catch (error) {
391
+ return { kind: "error", error };
392
+ }
393
+ }
394
+ function writeQueueSections(boardResult) {
395
+ if (boardResult.kind === "error") {
396
+ writeSection("Queue");
397
+ writeOutput(`unavailable: ${errorMessage(boardResult.error)}`);
398
+ return;
399
+ }
400
+ // Only groundcrew-eligible Todos are dispatchable; non-eligible ones lack
401
+ // a repo or model, so `crew run` would skip them.
402
+ const todos = boardResult.issues.filter(isTodoSourceIssue).filter(isGroundcrewIssue);
403
+ const ready = todos.filter((i) => !hasOpenBlocker(i));
404
+ const blocked = todos.filter(hasOpenBlocker);
405
+ // Hide the section entirely when nothing's queued and nothing's blocked.
406
+ if (ready.length > 0) {
407
+ writeSection("Queue");
408
+ for (const [index, issue] of ready.entries()) {
409
+ if (index > 0) {
410
+ writeOutput();
411
+ }
412
+ writeQueueIssue(issue);
413
+ }
414
+ }
415
+ if (blocked.length > 0) {
416
+ writeSection("Blocked");
417
+ for (const [index, issue] of blocked.entries()) {
418
+ if (index > 0) {
419
+ writeOutput();
420
+ }
421
+ writeQueueIssue(issue);
422
+ writeOutput(inventoryField("blocked by", describeOpenBlockers(issue)));
423
+ }
424
+ }
425
+ }
426
+ function inProgressCount(issues) {
427
+ return issues.filter((issue) => issue.status === "in-progress").length;
158
428
  }
159
429
  async function writeInventoryStatus(config) {
160
- writeOutput("groundcrew status");
161
- writeOutput("=================");
430
+ // Banner ("groundcrew status\n=================") dropped: the command
431
+ // you just ran already tells you what report you're looking at, and the
432
+ // section headers (`Worktrees`, `Queue`, etc.) carry the visual anchors.
433
+ const boardResultPromise = fetchBoardForStatus(config);
162
434
  const probe = await withLogOutputSuppressed(async () => await workspaces.probe(config));
163
- writeInventoryWorktrees(config, probe);
164
- writeInventoryWorkspaces(probe);
435
+ await writeInventoryWorktrees(config, probe);
436
+ const worktreeTickets = new Set(worktrees.list(config).map((entry) => entry.ticket));
437
+ writeStraySessions(probe, worktreeTickets);
438
+ const boardResult = await boardResultPromise;
439
+ if (boardResult.kind === "ok") {
440
+ const used = inProgressCount(boardResult.issues);
441
+ writeOutput();
442
+ writeOutput(`slots: ${used}/${config.orchestrator.maximumInProgress} used`);
443
+ }
444
+ writeQueueSections(boardResult);
165
445
  }
166
446
  export async function status(config, options = {}) {
167
447
  const ticket = options.ticket?.trim();
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAIL,KAAK,KAAK,IAAI,cAAc,EAE5B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAKL,KAAK,KAAK,IAAI,WAAW,EAE1B,MAAM,YAAY,CAAC;AAGpB;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,YAAY,EAAE,MAAM,CAAC;CACtB;AAkFD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAsB7F;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,cAAc,GACtB,YAAY,CA+Ed"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAIL,KAAK,KAAK,IAAI,cAAc,EAE5B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAKL,KAAK,KAAK,IAAI,WAAW,EAE1B,MAAM,YAAY,CAAC;AAGpB;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,YAAY,EAAE,MAAM,CAAC;CACtB;AAkFD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAuB7F;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,cAAc,GACtB,YAAY,CAgFd"}
@@ -107,6 +107,7 @@ export function toCanonicalIssue(linearIssue, sourceName) {
107
107
  updatedAt: linearIssue.updatedAt,
108
108
  blockers: linearIssue.blockers.map((b) => toCanonicalBlocker(b, sourceName)),
109
109
  hasMoreBlockers: linearIssue.hasMoreBlockers,
110
+ url: linearIssue.url,
110
111
  sourceRef,
111
112
  };
112
113
  }
@@ -172,6 +173,7 @@ export function createLinearTicketSource(config, context) {
172
173
  updatedAt: new Date().toISOString(),
173
174
  blockers: [],
174
175
  hasMoreBlockers: false,
176
+ url: resolved.url,
175
177
  sourceRef,
176
178
  };
177
179
  },
@@ -46,6 +46,8 @@ export interface Issue {
46
46
  teamId: string;
47
47
  blockers: Blocker[];
48
48
  hasMoreBlockers: boolean;
49
+ /** Linear `Issue.url` — direct web link to the ticket. */
50
+ url: string;
49
51
  }
50
52
  /**
51
53
  * `Issue` narrowed to "this ticket is for groundcrew". Consumers operate on
@@ -113,6 +115,7 @@ interface ResolvedIssue {
113
115
  stateType: string;
114
116
  status: string;
115
117
  statusId: string;
118
+ url: string;
116
119
  }
117
120
  export interface RawLinearIssue {
118
121
  uuid: string;
@@ -135,6 +138,8 @@ export interface RawLinearIssue {
135
138
  * reporting "would dispatch."
136
139
  */
137
140
  hasChildren: boolean;
141
+ /** Linear `Issue.url` — direct web link to the ticket. */
142
+ url: string;
138
143
  }
139
144
  export declare function fetchBlockersForTicket(arguments_: {
140
145
  client: LinearClient;
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/fetch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/fetch.ts"],"names":[],"mappings":"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"}
@@ -84,6 +84,7 @@ async function fetchBoard(client, config) {
84
84
  title
85
85
  description
86
86
  updatedAt
87
+ url
87
88
  state { id name type }
88
89
  team { id key }
89
90
  assignee { name }
@@ -177,6 +178,7 @@ function buildLinearIssue(input) {
177
178
  repository: input.repository,
178
179
  model: input.model,
179
180
  teamId: input.teamId,
181
+ url: input.url,
180
182
  blockers: blockersFromRelations(input.inverseRelations?.nodes ?? []),
181
183
  hasMoreBlockers: input.inverseRelations?.pageInfo.hasNextPage ?? false,
182
184
  };
@@ -209,6 +211,7 @@ function issueFromNode(node, config) {
209
211
  repository,
210
212
  model,
211
213
  teamId: node.team?.id ?? "",
214
+ url: node.url,
212
215
  inverseRelations: node.inverseRelations,
213
216
  });
214
217
  }
@@ -255,6 +258,7 @@ export async function fetchRawLinearIssue(arguments_) {
255
258
  id
256
259
  title
257
260
  description
261
+ url
258
262
  team { id }
259
263
  state { id name type }
260
264
  children { nodes { id } }
@@ -295,6 +299,7 @@ export async function fetchRawLinearIssue(arguments_) {
295
299
  blockers: blockersFromRelations(issue.inverseRelations?.nodes ?? []),
296
300
  hasMoreBlockers: issue.inverseRelations?.pageInfo.hasNextPage ?? false,
297
301
  hasChildren: (issue.children?.nodes.length ?? 0) > 0,
302
+ url: issue.url,
298
303
  };
299
304
  }
300
305
  export async function fetchInProgressIssueCount(arguments_) {
@@ -371,6 +376,7 @@ export async function fetchResolvedIssue(arguments_) {
371
376
  stateType: raw.stateType,
372
377
  status: raw.stateName,
373
378
  statusId: raw.stateId,
379
+ url: raw.url,
374
380
  };
375
381
  }
376
382
  export function warnIfDisabledFallback(ticket, modelResolution, config) {
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAGL,KAAK,KAAK,IAAI,cAAc,EAC5B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAyBrB,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAsB3F;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,cAAc,GACvB,YAAY,CAgFd"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAGL,KAAK,KAAK,IAAI,cAAc,EAC5B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAyBrB,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAuB3F;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,cAAc,GACvB,YAAY,CAgFd"}
@@ -50,6 +50,7 @@ export function toCanonicalIssue(shellIssue, sourceName) {
50
50
  updatedAt: shellIssue.updatedAt,
51
51
  blockers,
52
52
  hasMoreBlockers: shellIssue.hasMoreBlockers,
53
+ ...(shellIssue.url === undefined ? {} : { url: shellIssue.url }),
53
54
  sourceRef: shellIssue.sourceRef,
54
55
  };
55
56
  }
@@ -41,6 +41,7 @@ export declare const shellIssueSchema: z.ZodObject<{
41
41
  nativeStatus: z.ZodOptional<z.ZodString>;
42
42
  }, z.core.$strip>>;
43
43
  hasMoreBlockers: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
44
+ url: z.ZodOptional<z.ZodURL>;
44
45
  sourceRef: z.ZodUnknown;
45
46
  }, z.core.$strip>;
46
47
  export type ShellIssue = z.infer<typeof shellIssueSchema>;
@@ -76,6 +77,7 @@ export declare const shellFetchOutputSchema: z.ZodArray<z.ZodObject<{
76
77
  nativeStatus: z.ZodOptional<z.ZodString>;
77
78
  }, z.core.$strip>>;
78
79
  hasMoreBlockers: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
80
+ url: z.ZodOptional<z.ZodURL>;
79
81
  sourceRef: z.ZodUnknown;
80
82
  }, z.core.$strip>>;
81
83
  export declare const shellAdapterConfigSchema: z.ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAA4B,CAAC;AAEhE,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;iBAwBnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiB3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAA4B,CAAC;AAEhE,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;iBAwBnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
@@ -28,6 +28,11 @@ export const shellIssueSchema = z.object({
28
28
  updatedAt: z.string(),
29
29
  blockers: z.array(shellBlockerSchema),
30
30
  hasMoreBlockers: z.boolean().optional().default(false),
31
+ /**
32
+ * Direct web URL for the ticket. Optional so scripts can omit it without
33
+ * breaking; `crew status` falls back to displaying just the id.
34
+ */
35
+ url: z.url().optional(),
31
36
  sourceRef: z.unknown(),
32
37
  });
33
38
  export const shellFetchOutputSchema = z.array(shellIssueSchema);
@@ -1 +1 @@
1
- {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;GAGG;AACH,eAAO,MAAM,aAAa,mFACwD,CAAC;AAiCnF,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CAK7E"}
1
+ {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;GAGG;AACH,eAAO,MAAM,aAAa,mFACwD,CAAC;AAiCnF,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CAQ7E"}
@@ -65,20 +65,35 @@ export function buildLaunchCommand(arguments_) {
65
65
  if (arguments_.runner === "sdx") {
66
66
  return buildSdxLaunchCommand(arguments_);
67
67
  }
68
- return buildHostLaunchCommand(arguments_);
68
+ if (shouldWrapWithSafehouse(arguments_)) {
69
+ return buildSafehouseLaunchCommand(arguments_);
70
+ }
71
+ return buildUnwrappedHostLaunchCommand(arguments_);
69
72
  }
70
- function buildHostLaunchCommand(arguments_) {
73
+ /**
74
+ * The Safehouse wrap applies only when `runner === "safehouse"` and `cmd` does
75
+ * not already invoke `safehouse` itself. A `safehouse …` cmd owns its own
76
+ * sandbox flags, and we can't splice setup into a command we don't control, so
77
+ * those (and the `none` runner) fall through to the unwrapped host path.
78
+ */
79
+ function shouldWrapWithSafehouse(arguments_) {
80
+ if (arguments_.runner !== "safehouse") {
81
+ return false;
82
+ }
83
+ return !/^safehouse(\s|$)/.test(arguments_.definition.cmd);
84
+ }
85
+ /**
86
+ * Unsandboxed host launch (`runner === "none"`, or a `safehouse …` cmd that
87
+ * brings its own wrap). Setup, secret sourcing, and the agent all run on the
88
+ * host shell because there is no groundcrew-managed sandbox to run them inside.
89
+ */
90
+ function buildUnwrappedHostLaunchCommand(arguments_) {
71
91
  const promptDir = dirname(arguments_.promptFile);
72
92
  const agentCmd = renderAgentCommand({
73
93
  agentCmd: arguments_.definition.cmd,
74
94
  worktreeDir: arguments_.worktreeDir,
75
95
  sandboxName: "",
76
96
  });
77
- const wrapped = wrapAgentForHostRunner({
78
- runner: arguments_.runner,
79
- rawCmd: arguments_.definition.cmd,
80
- agentCmd,
81
- });
82
97
  const lines = [`cd ${shellSingleQuote(arguments_.worktreeDir)}`];
83
98
  if (arguments_.secretsFile !== undefined) {
84
99
  lines.push(sourceSecretsLine(arguments_.secretsFile));
@@ -87,28 +102,40 @@ function buildHostLaunchCommand(arguments_) {
87
102
  if (arguments_.secretsFile !== undefined) {
88
103
  lines.push(unsetSecretsLine());
89
104
  }
90
- lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec ${wrapped} "$_p"`);
105
+ lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec ${agentCmd} "$_p"`);
91
106
  return lines.join(" && ");
92
107
  }
93
- function wrapAgentForHostRunner(arguments_) {
94
- if (arguments_.runner === "none") {
95
- return arguments_.agentCmd;
96
- }
97
- // buildLaunchCommand routes `sdx` through buildSdxLaunchCommand, so the
98
- // only remaining shape here is `safehouse`. Treat the explicit branch as
99
- // the safehouse wrap to keep this function readable; the `sdx` arm exists
100
- // only to satisfy TS's exhaustiveness checker.
101
- /* v8 ignore next 3 @preserve -- buildLaunchCommand short-circuits sdx before calling this helper */
102
- if (arguments_.runner === "sdx") {
103
- return arguments_.agentCmd;
108
+ /**
109
+ * Safehouse launch. Setup runs *inside* the `safehouse-clearance` wrap (mirroring
110
+ * the sdx runner) so the repo's `.groundcrew/setup.sh` and its `npm install` are
111
+ * filesystem-isolated and egress-restricted, rather than running on the bare host.
112
+ *
113
+ * Build secrets are sourced into the host launch shell so Safehouse can forward
114
+ * them into the sandbox via `--env-pass` (Safehouse's `--env=FILE` mode otherwise
115
+ * strips them); they're `unset` inside the wrap after setup so the agent process
116
+ * never inherits them. The host keeps only `cd`, the prompt read, and the wrap exec.
117
+ */
118
+ function buildSafehouseLaunchCommand(arguments_) {
119
+ const promptDir = dirname(arguments_.promptFile);
120
+ const agentCmd = renderAgentCommand({
121
+ agentCmd: arguments_.definition.cmd,
122
+ worktreeDir: arguments_.worktreeDir,
123
+ sandboxName: "",
124
+ });
125
+ const innerParts = [setupWithStatusReporting(SETUP_COMMAND)];
126
+ if (arguments_.secretsFile !== undefined) {
127
+ innerParts.push(unsetSecretsLine());
104
128
  }
105
- // safehouse: skip the wrap if `cmd` already starts with `safehouse` so
106
- // legacy configs don't double-wrap.
107
- const cmdStartsWithSafehouse = /^safehouse(\s|$)/.test(arguments_.rawCmd);
108
- if (cmdStartsWithSafehouse) {
109
- return arguments_.agentCmd;
129
+ innerParts.push(`exec ${agentCmd} "$@"`);
130
+ const innerCommand = innerParts.join("; ");
131
+ // Trailing space keeps the flag and `sh` separated; empty when no secrets.
132
+ const envPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
133
+ const lines = [`cd ${shellSingleQuote(arguments_.worktreeDir)}`];
134
+ if (arguments_.secretsFile !== undefined) {
135
+ lines.push(sourceSecretsLine(arguments_.secretsFile));
110
136
  }
111
- return [shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH), arguments_.agentCmd].join(" ");
137
+ lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec ${shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH)} ${envPassFlag}sh -lc ${shellSingleQuote(innerCommand)} sh "$_p"`);
138
+ return lines.join(" && ");
112
139
  }
113
140
  function buildSdxLaunchCommand(arguments_) {
114
141
  /* v8 ignore next 5 @preserve -- setupWorkspace passes sandboxName + sandbox config when picking sdx; missing fields are programmer errors */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Best-effort lookup of GitHub pull requests for a worktree branch via the
3
+ * `gh` CLI. `crew status` uses this to surface PR links inline; failures
4
+ * (gh not on PATH, not authenticated, non-GitHub remote) are silent — the
5
+ * caller falls back to omitting the row.
6
+ */
7
+ export interface PullRequestSummary {
8
+ url: string;
9
+ number: number;
10
+ /** Lowercased lifecycle: "open" | "merged" | "closed". */
11
+ state: string;
12
+ title: string;
13
+ }
14
+ interface LookupArgs {
15
+ /** GitHub `owner/repo` slug. */
16
+ repository: string;
17
+ /** Branch name to filter PRs by. */
18
+ branchName: string;
19
+ signal?: AbortSignal;
20
+ }
21
+ export declare function findPullRequestsForBranch(arguments_: LookupArgs): Promise<readonly PullRequestSummary[]>;
22
+ export {};
23
+ //# sourceMappingURL=pullRequests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pullRequests.d.ts","sourceRoot":"","sources":["../../src/lib/pullRequests.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AASD,UAAU,UAAU;IAClB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAgDD,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CA2BxC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Best-effort lookup of GitHub pull requests for a worktree branch via the
3
+ * `gh` CLI. `crew status` uses this to surface PR links inline; failures
4
+ * (gh not on PATH, not authenticated, non-GitHub remote) are silent — the
5
+ * caller falls back to omitting the row.
6
+ */
7
+ import { runCommandAsync } from "./commandRunner.js";
8
+ const GH_PR_LIST_LIMIT = 5;
9
+ const STATE_MAP = {
10
+ OPEN: "open",
11
+ MERGED: "merged",
12
+ CLOSED: "closed",
13
+ };
14
+ function parsePullRequests(output) {
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(output);
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ if (!Array.isArray(parsed)) {
23
+ return [];
24
+ }
25
+ const summaries = [];
26
+ for (const entry of parsed) {
27
+ if (!isRawPullRequest(entry)) {
28
+ continue;
29
+ }
30
+ summaries.push({
31
+ url: entry.url,
32
+ number: entry.number,
33
+ state: STATE_MAP[entry.state] ?? entry.state.toLowerCase(),
34
+ title: entry.title,
35
+ });
36
+ }
37
+ return summaries;
38
+ }
39
+ function isRawPullRequest(value) {
40
+ if (typeof value !== "object" || value === null) {
41
+ return false;
42
+ }
43
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- narrowing untyped JSON.parse output to a record so we can probe its keys
44
+ const record = value;
45
+ return (typeof record["url"] === "string" &&
46
+ typeof record["number"] === "number" &&
47
+ typeof record["state"] === "string" &&
48
+ typeof record["title"] === "string");
49
+ }
50
+ export async function findPullRequestsForBranch(arguments_) {
51
+ const { repository, branchName, signal } = arguments_;
52
+ try {
53
+ const output = await runCommandAsync("gh", [
54
+ "pr",
55
+ "list",
56
+ "--repo",
57
+ repository,
58
+ "--head",
59
+ branchName,
60
+ "--state",
61
+ "all",
62
+ "--limit",
63
+ String(GH_PR_LIST_LIMIT),
64
+ "--json",
65
+ "url,number,state,title",
66
+ ], signal === undefined ? {} : { signal });
67
+ return parsePullRequests(output);
68
+ }
69
+ catch {
70
+ // gh not installed / not authenticated / non-GitHub remote / network
71
+ // error / etc. All resolve to "no PR info available" for display.
72
+ return [];
73
+ }
74
+ }
@@ -13,6 +13,19 @@ export interface RunState {
13
13
  resumeCount: number;
14
14
  reason?: string;
15
15
  detail?: string;
16
+ /**
17
+ * Ticket title at dispatch time. Cached so `crew status` can render it
18
+ * without re-hitting the ticket source; lifecycle transitions
19
+ * (resume/interrupt) that omit the field preserve the on-disk value.
20
+ */
21
+ title?: string;
22
+ /**
23
+ * Direct ticket URL at dispatch time. Same caching rationale as `title`;
24
+ * the source adapter populates it when it can (e.g., Linear), otherwise
25
+ * the field stays undefined and `crew status` falls back to displaying
26
+ * just the ticket id.
27
+ */
28
+ url?: string;
16
29
  }
17
30
  export interface RunStateDraft {
18
31
  ticket: string;
@@ -25,6 +38,8 @@ export interface RunStateDraft {
25
38
  reason?: string;
26
39
  detail?: string;
27
40
  resumeCount?: number;
41
+ title?: string;
42
+ url?: string;
28
43
  }
29
44
  export interface RecordRunStateInput {
30
45
  config: ResolvedConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"runState.d.ts","sourceRoot":"","sources":["../../src/lib/runState.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE3F,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,iBAAiB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC,GAAG;QACvD,KAAK,EAAE,iBAAiB,CAAC;KAC1B,CAAC;CACH;AAaD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,GAAG,MAAM,CAEjF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5F;AA+ED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAYzF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,CAmBnE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,GAAG,SAAS,CAc/E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAE3E"}
1
+ {"version":3,"file":"runState.d.ts","sourceRoot":"","sources":["../../src/lib/runState.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE3F,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,iBAAiB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC,GAAG;QACvD,KAAK,EAAE,iBAAiB,CAAC;KAC1B,CAAC;CACH;AAaD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,GAAG,MAAM,CAEjF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5F;AAmFD,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAYzF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,CA0BnE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,GAAG,SAAS,CAc/E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAE3E"}
@@ -46,6 +46,8 @@ function parseRunState(value) {
46
46
  const updatedAt = stringField(value, "updatedAt");
47
47
  const reason = stringField(value, "reason");
48
48
  const detail = stringField(value, "detail");
49
+ const title = stringField(value, "title");
50
+ const url = stringField(value, "url");
49
51
  if (ticket === undefined ||
50
52
  repository === undefined ||
51
53
  model === undefined ||
@@ -73,6 +75,8 @@ function parseRunState(value) {
73
75
  resumeCount,
74
76
  ...(reason === undefined ? {} : { reason }),
75
77
  ...(detail === undefined ? {} : { detail }),
78
+ ...(title === undefined ? {} : { title }),
79
+ ...(url === undefined ? {} : { url }),
76
80
  };
77
81
  }
78
82
  function writeState(config, state) {
@@ -100,6 +104,11 @@ export function readRunState(config, ticket) {
100
104
  export function recordRunState(input) {
101
105
  const existing = readRunState(input.config, input.state.ticket);
102
106
  const timestamp = nowIso();
107
+ // Resume/interrupt callers don't know the title or url, so they omit
108
+ // them. Fall back to the on-disk value so cached display fields survive
109
+ // transitions.
110
+ const title = input.state.title ?? existing?.title;
111
+ const url = input.state.url ?? existing?.url;
103
112
  const state = {
104
113
  ticket: ticketKey(input.state.ticket),
105
114
  repository: input.state.repository,
@@ -113,6 +122,8 @@ export function recordRunState(input) {
113
122
  resumeCount: input.state.resumeCount ?? existing?.resumeCount ?? 0,
114
123
  ...(input.state.reason === undefined ? {} : { reason: input.state.reason }),
115
124
  ...(input.state.detail === undefined ? {} : { detail: input.state.detail }),
125
+ ...(title === undefined ? {} : { title }),
126
+ ...(url === undefined ? {} : { url }),
116
127
  };
117
128
  writeState(input.config, state);
118
129
  return state;
@@ -67,6 +67,13 @@ export interface Issue {
67
67
  updatedAt: string;
68
68
  blockers: Blocker[];
69
69
  hasMoreBlockers: boolean;
70
+ /**
71
+ * Direct web URL for the ticket on the source system, when the adapter
72
+ * knows one. `undefined` when the source can't produce a public URL (e.g.,
73
+ * a shell script that omits the `url` field). Display-only — never
74
+ * branched on.
75
+ */
76
+ url?: string;
70
77
  /** Adapter-private. Consumers MUST NOT inspect; only the producing adapter reads it. */
71
78
  sourceRef: unknown;
72
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ticketSource.d.ts","sourceRoot":"","sources":["../../src/lib/ticketSource.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ticketSource.d.ts","sourceRoot":"","sources":["../../src/lib/ticketSource.ts"],"names":[],"mappings":"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"}
@@ -22,12 +22,13 @@ interface ResolveArguments {
22
22
  }
23
23
  export declare function resolveWorkspaceKind(arguments_: ResolveArguments): WorkspaceResolution;
24
24
  declare function probeWorkspaces(config: ResolvedConfig, signal?: AbortSignal): Promise<WorkspaceProbe>;
25
+ declare function accessHintForWorkspace(config: ResolvedConfig, name: string, signal?: AbortSignal): Promise<WorkspaceAccessHint | undefined>;
25
26
  declare function interruptWorkspace(config: ResolvedConfig, name: string, signal?: AbortSignal): Promise<WorkspaceInterruptResult>;
26
27
  export declare const workspaces: {
27
28
  open(config: ResolvedConfig, spec: OpenSpec, signal?: AbortSignal): Promise<void>;
28
29
  probe: typeof probeWorkspaces;
29
30
  close(config: ResolvedConfig, name: string, signal?: AbortSignal): Promise<WorkspaceCloseResult>;
30
31
  interrupt: typeof interruptWorkspace;
31
- accessHint(config: ResolvedConfig, name: string, signal?: AbortSignal): Promise<WorkspaceAccessHint | undefined>;
32
+ accessHint: typeof accessHintForWorkspace;
32
33
  };
33
34
  //# sourceMappingURL=workspaces.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE1E,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AAsDD,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,iBAAe,kBAAkB,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SACD,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC;IAIhC,SAAS;IACH,UAAU,SACN,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;CAI5C,CAAC"}
1
+ {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE1E,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AAsDD,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,iBAAe,sBAAsB,CACnC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAG1C;AAED,iBAAe,kBAAkB,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SACD,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC;IAIhC,SAAS;IACT,UAAU;CACX,CAAC"}
@@ -76,6 +76,10 @@ async function probeWorkspaces(config, signal) {
76
76
  }
77
77
  return { kind: "ok", names: new Set(raw.map((ws) => ws.name)) };
78
78
  }
79
+ async function accessHintForWorkspace(config, name, signal) {
80
+ const adapter = await adapterFor(config, signal);
81
+ return adapter.accessHint(name);
82
+ }
79
83
  async function interruptWorkspace(config, name, signal) {
80
84
  const probe = await probeWorkspaces(config, signal);
81
85
  if (probe.kind === "unavailable") {
@@ -103,8 +107,5 @@ export const workspaces = {
103
107
  return await adapter.close(name, signal);
104
108
  },
105
109
  interrupt: interruptWorkspace,
106
- async accessHint(config, name, signal) {
107
- const adapter = await adapterFor(config, signal);
108
- return adapter.accessHint(name);
109
- },
110
+ accessHint: accessHintForWorkspace,
110
111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.2.3",
3
+ "version": "4.3.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",
@@ -74,7 +74,7 @@
74
74
  "zod": "4.4.3"
75
75
  },
76
76
  "devDependencies": {
77
- "@clipboard-health/ai-rules": "2.18.7",
77
+ "@clipboard-health/ai-rules": "2.21.0",
78
78
  "@clipboard-health/oxlint-config": "1.9.4",
79
79
  "@nx/js": "22.7.2",
80
80
  "@tsconfig/node24": "24.0.4",