@clipboard-health/groundcrew 4.0.0 → 4.0.2

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/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAgOA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA6MA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCvD"}
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
8
8
  import { sandboxCli } from "./commands/sandbox/index.js";
9
9
  import { setupReposCli } from "./commands/setupRepos.js";
10
10
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
11
+ import { statusCli } from "./commands/status.js";
11
12
  import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
12
13
  import { computeUpgradeNudge, defaultUpgradeCheckCachePath, fetchLatestVersion, } from "./lib/upgrade.js";
13
14
  import { errorMessage, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
@@ -80,30 +81,10 @@ async function maybeRunUpgradeNudge(metadata) {
80
81
  }
81
82
  }
82
83
  async function doctorCli(argv) {
83
- let ticket;
84
- const remainingArgs = [];
85
- for (let index = 0; index < argv.length; index += 1) {
86
- const argument = argv[index];
87
- if (argument === "--ticket") {
88
- ticket = readTicketArgument(argv, index, "doctor");
89
- index += 1;
90
- continue;
91
- }
92
- if (argument === "--no-linear" || argument === "--no-fetch") {
93
- remainingArgs.push(argument);
94
- continue;
95
- }
96
- throw new Error(`crew doctor: unknown argument: ${argument}`);
84
+ if (argv.length > 0) {
85
+ throw new Error("Usage: crew doctor");
97
86
  }
98
- if (ticket === undefined) {
99
- if (remainingArgs.length > 0) {
100
- throw new Error(`crew doctor: ${remainingArgs[0]} requires --ticket (host doctor mode has no flags)`);
101
- }
102
- const ok = await doctor();
103
- process.exitCode = ok ? process.exitCode : 1;
104
- return;
105
- }
106
- const ok = await doctor({ ticket, ticketArgv: remainingArgs });
87
+ const ok = await doctor();
107
88
  process.exitCode = ok ? process.exitCode : 1;
108
89
  }
109
90
  const SUBCOMMANDS = {
@@ -118,10 +99,15 @@ const SUBCOMMANDS = {
118
99
  invoke: runCli,
119
100
  },
120
101
  doctor: {
121
- summary: "Verify prereqs, or diagnose one ticket with --ticket (full lifecycle: dispatch eligibility + local-state recovery)",
122
- usage: "[--ticket <ticket> [--no-linear] [--no-fetch]]",
102
+ summary: "Verify host prerequisites (PATH tools, config validity, Linear reachability)",
103
+ usage: "",
123
104
  invoke: doctorCli,
124
105
  },
106
+ status: {
107
+ summary: "Print read-only groundcrew state, or one ticket's local/Linear status",
108
+ usage: "[<ticket>]",
109
+ invoke: statusCli,
110
+ },
125
111
  cleanup: {
126
112
  summary: "Tear down a worktree",
127
113
  usage: "[--force] <ticket>",
@@ -2,10 +2,5 @@
2
2
  * doctor — verify groundcrew prerequisites against the resolved config.
3
3
  * Returns true if every required check passes; false otherwise.
4
4
  */
5
- export interface DoctorOptions {
6
- ticket?: string;
7
- /** Extra flags after `--ticket <id>`; currently `--no-linear` and `--no-fetch`. */
8
- ticketArgv?: string[];
9
- }
10
- export declare function doctor(options?: DoctorOptions): Promise<boolean>;
5
+ export declare function doctor(): Promise<boolean>;
11
6
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4BH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAgHD,wBAAsB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAK1E"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoJH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA8E/C"}
@@ -4,12 +4,12 @@
4
4
  */
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { loadConfig, } from "../lib/config.js";
7
+ import { createBoardSource } from "../lib/boardSource.js";
7
8
  import { detectHostCapabilities, which } from "../lib/host.js";
8
9
  import { resolveLocalRunner } from "../lib/localRunner.js";
9
10
  import { gatedModels } from "../lib/usage.js";
10
- import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
11
+ import { errorMessage, getLinearClient, resolveLinearApiKey, writeOutput } from "../lib/util.js";
11
12
  import { resolveWorkspaceKind } from "../lib/workspaces.js";
12
- import { parseTicketDoctorFlags, runTicketDoctor } from "./ticketDoctor.js";
13
13
  // Tokenization stops after this many non-flag tokens. Two is enough to
14
14
  // catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
15
15
  const MAX_TOKENS_PER_CMD = 2;
@@ -26,22 +26,33 @@ async function checkCmd(cmd, required, hint) {
26
26
  }
27
27
  return result;
28
28
  }
29
- function checkLinearApiKey() {
29
+ async function checkLinearReachability(config) {
30
30
  const resolved = resolveLinearApiKey();
31
- if (resolved !== undefined) {
31
+ if (resolved === undefined) {
32
32
  return {
33
- name: "linear api key",
33
+ name: "linear reachability",
34
+ ok: false,
35
+ required: true,
36
+ hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
37
+ };
38
+ }
39
+ try {
40
+ await createBoardSource({ config, client: getLinearClient() }).verify();
41
+ return {
42
+ name: "linear reachability",
34
43
  ok: true,
35
44
  required: true,
36
45
  hint: `set via $${resolved.source}`,
37
46
  };
38
47
  }
39
- return {
40
- name: "linear api key",
41
- ok: false,
42
- required: true,
43
- hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
44
- };
48
+ catch (error) {
49
+ return {
50
+ name: "linear reachability",
51
+ ok: false,
52
+ required: true,
53
+ hint: errorMessage(error),
54
+ };
55
+ }
45
56
  }
46
57
  function checkDir(path, label) {
47
58
  // statSync can throw on permission errors or path races; surface those
@@ -120,31 +131,7 @@ function format(check) {
120
131
  const hint = check.hint !== undefined && check.hint.length > 0 ? ` — ${check.hint}` : "";
121
132
  return `${tag}${check.name}${hint}`;
122
133
  }
123
- export async function doctor(options = {}) {
124
- if (options.ticket !== undefined) {
125
- return await doctorTicket(options.ticket, options.ticketArgv ?? []);
126
- }
127
- return await doctorHost();
128
- }
129
- async function doctorTicket(ticket, ticketArgv) {
130
- try {
131
- const flags = parseTicketDoctorFlags(ticketArgv);
132
- return await runTicketDoctor({
133
- ticket,
134
- doLinear: flags.doLinear,
135
- doFetch: flags.doFetch,
136
- });
137
- }
138
- catch (error) {
139
- const displayTicket = ticket.toUpperCase();
140
- const header = `groundcrew doctor --ticket ${displayTicket}`;
141
- writeOutput(header);
142
- writeOutput("=".repeat(header.length));
143
- writeOutput(`[--] config: ${errorMessage(error)}`);
144
- return false;
145
- }
146
- }
147
- async function doctorHost() {
134
+ export async function doctor() {
148
135
  writeOutput("groundcrew doctor");
149
136
  writeOutput("=================");
150
137
  let config;
@@ -174,7 +161,7 @@ async function doctorHost() {
174
161
  const workspaceOutcome = resolveWorkspaceOutcome(config, host);
175
162
  reportWorkspaceKind(config, workspaceOutcome);
176
163
  const checks = [
177
- checkLinearApiKey(),
164
+ await checkLinearReachability(config),
178
165
  await checkCmd("git", true, "https://git-scm.com/"),
179
166
  ...(await workspaceChecks(workspaceOutcome)),
180
167
  checkDir(config.workspace.projectDir, "workspace.projectDir"),
@@ -100,7 +100,7 @@ export async function interruptWorkspace(config, options) {
100
100
  },
101
101
  });
102
102
  log(`Interrupted ${ticket}; worktree preserved at ${source.worktreeDir}`);
103
- log(`Next: crew doctor --ticket ${ticket}`);
103
+ log(`Next: crew status ${ticket}`);
104
104
  }
105
105
  export async function interruptWorkspaceCli(argv) {
106
106
  const config = await loadConfig();
@@ -0,0 +1,7 @@
1
+ import { type ResolvedConfig } from "../lib/config.ts";
2
+ export interface StatusOptions {
3
+ ticket?: string;
4
+ }
5
+ export declare function status(config: ResolvedConfig, options?: StatusOptions): Promise<void>;
6
+ export declare function statusCli(argv: string[]): Promise<void>;
7
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAWnE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuLD,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"}
@@ -0,0 +1,178 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fetchRawLinearIssue } from "../lib/boardSource.js";
3
+ import { loadConfig } from "../lib/config.js";
4
+ import { readRunState } from "../lib/runState.js";
5
+ import { errorMessage, getLinearClient, withLogOutputSuppressed, writeOutput, } from "../lib/util.js";
6
+ import { workspaces } from "../lib/workspaces.js";
7
+ import { worktrees } from "../lib/worktrees.js";
8
+ const RECENT_LOG_LINE_COUNT = 10;
9
+ function escapeRegExp(value) {
10
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
11
+ }
12
+ function ticketLinePattern(ticket) {
13
+ return new RegExp(`(^|[^a-z0-9])${escapeRegExp(ticket)}([^a-z0-9]|$)`, "i");
14
+ }
15
+ function parseArguments(argv) {
16
+ const [ticket, ...extras] = argv;
17
+ if (extras.length > 0 || ticket?.length === 0 || ticket?.startsWith("-") === true) {
18
+ throw new Error("Usage: crew status [<ticket>]");
19
+ }
20
+ return ticket === undefined ? {} : { ticket: ticket.toLowerCase() };
21
+ }
22
+ function writeSection(title) {
23
+ writeOutput();
24
+ writeOutput(title);
25
+ writeOutput("-".repeat(title.length));
26
+ }
27
+ function writeConfigSnapshot(config) {
28
+ writeSection("Config snapshot");
29
+ writeOutput(`projectDir: ${config.workspace.projectDir}`);
30
+ writeOutput(`repositories: ${config.workspace.knownRepositories.join(", ")}`);
31
+ writeOutput(`git: remote=${config.git.remote}; defaultBranch=${config.git.defaultBranch}`);
32
+ writeOutput(`workspaceKind: ${config.workspaceKind}`);
33
+ writeOutput(`local.runner: ${config.local.runner}`);
34
+ writeOutput(`models: default=${config.models.default}; enabled=${Object.keys(config.models.definitions).join(", ")}`);
35
+ writeOutput(`logFile: ${config.logging.file}`);
36
+ }
37
+ function formatDirtiness(dirtiness) {
38
+ if (dirtiness.kind === "dirty") {
39
+ return `dirty (${dirtiness.modified} modified, ${dirtiness.untracked} untracked)`;
40
+ }
41
+ return dirtiness.kind;
42
+ }
43
+ async function writeTicketWorktrees(config, ticket) {
44
+ writeSection("Worktree state");
45
+ const entries = worktrees.findByTicket(config, ticket);
46
+ if (entries.length === 0) {
47
+ writeOutput("(none)");
48
+ return;
49
+ }
50
+ for (const entry of entries) {
51
+ // oxlint-disable-next-line no-await-in-loop -- status output is easier to read in worktree order.
52
+ const dirtiness = await worktrees.probeWorkingTree({ worktreeDir: entry.dir });
53
+ writeOutput(`- ${entry.repository} ${entry.kind}`);
54
+ writeOutput(` ticket: ${entry.ticket}`);
55
+ writeOutput(` branch: ${entry.branchName}`);
56
+ writeOutput(` dir: ${entry.dir}`);
57
+ writeOutput(` git: ${formatDirtiness(dirtiness)}`);
58
+ }
59
+ }
60
+ function workspaceProbeUnavailableLine(probe) {
61
+ return probe.error === undefined
62
+ ? "Workspace probe unavailable"
63
+ : `Workspace probe unavailable: ${errorMessage(probe.error)}`;
64
+ }
65
+ function writeTicketWorkspace(probe, ticket) {
66
+ writeSection("Workspace probe");
67
+ if (probe.kind === "unavailable") {
68
+ writeOutput(workspaceProbeUnavailableLine(probe));
69
+ return;
70
+ }
71
+ writeOutput(`live: ${probe.names.has(ticket) ? "yes" : "no"}`);
72
+ }
73
+ function formatRunState(state) {
74
+ if (state === undefined) {
75
+ return "(none)";
76
+ }
77
+ const summary = `${state.state}; model=${state.model}; updated=${state.updatedAt}; resumes=${state.resumeCount}`;
78
+ const detail = state.reason ?? state.detail;
79
+ return detail === undefined ? summary : `${summary}; ${detail}`;
80
+ }
81
+ function recentTicketLogLines(config, ticket) {
82
+ let raw;
83
+ try {
84
+ raw = readFileSync(config.logging.file, "utf8");
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ const pattern = ticketLinePattern(ticket);
90
+ return raw
91
+ .split("\n")
92
+ .filter((line) => pattern.test(line))
93
+ .slice(-RECENT_LOG_LINE_COUNT);
94
+ }
95
+ async function linearStatus(ticket) {
96
+ try {
97
+ const issue = await fetchRawLinearIssue({ client: getLinearClient(), ticket });
98
+ return `${issue.stateName} (state.type=${issue.stateType ?? "unknown"}) — ${issue.title}`;
99
+ }
100
+ catch (error) {
101
+ return `unavailable: ${errorMessage(error)}`;
102
+ }
103
+ }
104
+ async function writeTicketStatus(config, rawTicket) {
105
+ const ticket = rawTicket.toLowerCase();
106
+ const displayTicket = ticket.toUpperCase();
107
+ writeOutput(`groundcrew status ${displayTicket}`);
108
+ writeOutput("=".repeat(`groundcrew status ${displayTicket}`.length));
109
+ writeOutput(`ticket: ${ticket}`);
110
+ writeConfigSnapshot(config);
111
+ await writeTicketWorktrees(config, ticket);
112
+ const workspaceProbe = await withLogOutputSuppressed(async () => await workspaces.probe(config));
113
+ writeTicketWorkspace(workspaceProbe, ticket);
114
+ writeSection("Run state");
115
+ writeOutput(formatRunState(readRunState(config, ticket)));
116
+ writeSection("Recent logs");
117
+ const logLines = recentTicketLogLines(config, ticket);
118
+ writeOutput(logLines.length === 0 ? "(none)" : logLines.join("\n"));
119
+ writeSection("Last Linear status");
120
+ writeOutput(await linearStatus(ticket));
121
+ }
122
+ function workspacePresence(probe, ticket) {
123
+ if (probe.kind === "unavailable") {
124
+ return "unknown";
125
+ }
126
+ return probe.names.has(ticket) ? "yes" : "no";
127
+ }
128
+ function writeInventoryWorktrees(config, probe) {
129
+ writeSection("Worktrees");
130
+ const entries = worktrees
131
+ .list(config)
132
+ .toSorted((left, right) => left.ticket.localeCompare(right.ticket));
133
+ if (entries.length === 0) {
134
+ writeOutput("(none)");
135
+ return;
136
+ }
137
+ const runStates = new Map();
138
+ for (const entry of entries) {
139
+ if (!runStates.has(entry.ticket)) {
140
+ runStates.set(entry.ticket, readRunState(config, entry.ticket));
141
+ }
142
+ const runState = runStates.get(entry.ticket);
143
+ writeOutput(`${entry.ticket} ${entry.repository} ${entry.kind} workspace=${workspacePresence(probe, entry.ticket)} run=${runState?.state ?? "none"}`);
144
+ writeOutput(` ${entry.branchName} ${entry.dir}`);
145
+ }
146
+ }
147
+ function writeInventoryWorkspaces(probe) {
148
+ writeSection("Live workspaces");
149
+ if (probe.kind === "unavailable") {
150
+ writeOutput(workspaceProbeUnavailableLine(probe));
151
+ return;
152
+ }
153
+ const names = [...probe.names].toSorted();
154
+ writeOutput(names.length === 0 ? "(none)" : names.join("\n"));
155
+ }
156
+ async function writeInventoryStatus(config) {
157
+ writeOutput("groundcrew status");
158
+ writeOutput("=================");
159
+ const probe = await withLogOutputSuppressed(async () => await workspaces.probe(config));
160
+ writeInventoryWorktrees(config, probe);
161
+ writeInventoryWorkspaces(probe);
162
+ }
163
+ export async function status(config, options = {}) {
164
+ const ticket = options.ticket?.trim();
165
+ if (ticket === undefined) {
166
+ await writeInventoryStatus(config);
167
+ return;
168
+ }
169
+ if (ticket.length === 0 || ticket.startsWith("-")) {
170
+ throw new Error("ticket must be a non-empty value");
171
+ }
172
+ await writeTicketStatus(config, ticket);
173
+ }
174
+ export async function statusCli(argv) {
175
+ const options = parseArguments(argv);
176
+ const config = await loadConfig();
177
+ await status(config, options);
178
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { interruptWorkspace, type InterruptWorkspaceOptions, } from "./commands/
5
5
  export { orchestrate, type OrchestratorOptions } from "./commands/orchestrator.ts";
6
6
  export { resumeWorkspace, type ResumeWorkspaceOptions } from "./commands/resumeWorkspace.ts";
7
7
  export { setupWorkspace, type SetupWorkspaceOptions } from "./commands/setupWorkspace.ts";
8
+ export { status, type StatusOptions } from "./commands/status.ts";
8
9
  export type { Config, ModelDefinition, ResolvedConfig, SourceConfig } from "./lib/config.ts";
9
10
  export { loadConfig } from "./lib/config.ts";
10
11
  export { readRunState, recordRunState, removeRunState, runStateDirectory, runStatePath, updateRunState, type RunLifecycleState, type RunState, } from "./lib/runState.ts";
@@ -15,6 +16,4 @@ export { buildSources, buildSourcesWith } from "./lib/buildSources.ts";
15
16
  export type { AdapterContext, AdapterDefinition } from "./lib/adapterDefinition.ts";
16
17
  export { adapterRegistry, type AdapterLoader, buildRegistry, buildSourceConfigSchema, listAdapterDirectories, } from "./lib/adapters/registry.ts";
17
18
  export { AmbiguousTicketError, type Blocker as CanonicalBlocker, type BoardState as CanonicalBoardState, type CanonicalStatus, type GroundcrewIssue as CanonicalGroundcrewIssue, type Issue as CanonicalIssue, isGroundcrewIssue as isCanonicalGroundcrewIssue, type TicketSource, } from "./lib/ticketSource.ts";
18
- export type { TicketCheck } from "./commands/ticketCheck.ts";
19
- export { ticketDoctor, type TicketDoctorDependencies, type TicketDoctorResult, type TicketDoctorVerdict, } from "./commands/ticketDoctor.ts";
20
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,eAAe,EACf,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,aAAa,EACb,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,oBAAoB,EACpB,KAAK,OAAO,IAAI,gBAAgB,EAChC,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,eAAe,EACpB,KAAK,eAAe,IAAI,wBAAwB,EAChD,KAAK,KAAK,IAAI,cAAc,EAC5B,iBAAiB,IAAI,0BAA0B,EAC/C,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EACL,YAAY,EACZ,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAClE,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,eAAe,EACf,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,aAAa,EACb,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,oBAAoB,EACpB,KAAK,OAAO,IAAI,gBAAgB,EAChC,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,eAAe,EACpB,KAAK,eAAe,IAAI,wBAAwB,EAChD,KAAK,KAAK,IAAI,cAAc,EAC5B,iBAAiB,IAAI,0BAA0B,EAC/C,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { interruptWorkspace, } from "./commands/interruptWorkspace.js";
5
5
  export { orchestrate } from "./commands/orchestrator.js";
6
6
  export { resumeWorkspace } from "./commands/resumeWorkspace.js";
7
7
  export { setupWorkspace } from "./commands/setupWorkspace.js";
8
+ export { status } from "./commands/status.js";
8
9
  export { loadConfig } from "./lib/config.js";
9
10
  export { readRunState, recordRunState, removeRunState, runStateDirectory, runStatePath, updateRunState, } from "./lib/runState.js";
10
11
  export { fetchBlockersForTicket, fetchInProgressIssueCount, fetchRawLinearIssue, fetchResolvedIssue, isIssueInProgress, isIssueTodo, isTerminalStateType, isTerminalStatusForBlocker, isTerminalStatusForIssue, resolveModelFor, resolveRepositoryFor, } from "./lib/boardSource.js";
@@ -13,4 +14,4 @@ export { createBoard } from "./lib/board.js";
13
14
  export { buildSources, buildSourcesWith } from "./lib/buildSources.js";
14
15
  export { adapterRegistry, buildRegistry, buildSourceConfigSchema, listAdapterDirectories, } from "./lib/adapters/registry.js";
15
16
  export { AmbiguousTicketError, isGroundcrewIssue as isCanonicalGroundcrewIssue, } from "./lib/ticketSource.js";
16
- export { ticketDoctor, } from "./commands/ticketDoctor.js";
17
+ // RepositoryResolutionError is exported via boardSource.ts above (single canonical location).
@@ -3,6 +3,7 @@ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
3
3
  export declare function writeOutput(message?: string): void;
4
4
  export declare function writeError(message: string): void;
5
5
  export declare function setLogFile(path: string | undefined): void;
6
+ export declare function withLogOutputSuppressed<T>(operation: () => Promise<T>): Promise<T>;
6
7
  export declare function log(message: string): void;
7
8
  type LogEventFieldValue = boolean | number | string | readonly string[] | undefined;
8
9
  export declare function logEvent(event: string, fields: Record<string, LogEventFieldValue>): void;
@@ -18,8 +19,8 @@ export declare function getLinearClient(): LinearClient;
18
19
  /**
19
20
  * Returns a zero-arg getter that lazily constructs (and caches) a Linear
20
21
  * client on first call. Used by CLI entry points that may not need the
21
- * client at all (e.g. `--no-linear`), so we avoid blowing up on a missing
22
- * API key when no Linear call is actually made. The factory is taken as a
22
+ * client at all, so we avoid blowing up on a missing API key when no Linear
23
+ * call is actually made. The factory is taken as a
23
24
  * parameter (rather than calling `getLinearClient` directly) so callers can
24
25
  * pass their own module-level import of `getLinearClient` — that binding
25
26
  * respects `vi.mock` intercepts, whereas an intra-module reference would
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAOD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAkBD,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKzC;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAWxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,QAAA,MAAM,sBAAsB,YAAI,2BAA2B,EAAE,gBAAgB,CAAU,CAAC;AAExF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,wBAAgB,mBAAmB,IAAI,oBAAoB,GAAG,SAAS,CAQtE;AAED,wBAAgB,eAAe,IAAI,YAAY,CAQ9C;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,YAAY,GAAG,MAAM,YAAY,CAMhF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAMzF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAQD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAED,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOxF;AAkBD,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAQzC;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAcxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,QAAA,MAAM,sBAAsB,YAAI,2BAA2B,EAAE,gBAAgB,CAAU,CAAC;AAExF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,wBAAgB,mBAAmB,IAAI,oBAAoB,GAAG,SAAS,CAQtE;AAED,wBAAgB,eAAe,IAAI,YAAY,CAQ9C;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,YAAY,GAAG,MAAM,YAAY,CAMhF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAMzF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
package/dist/lib/util.js CHANGED
@@ -35,9 +35,19 @@ export function writeError(message) {
35
35
  // so tests don't write to the host filesystem; the CLI arms it after
36
36
  // loadConfig() resolves `logging.file`.
37
37
  let logFilePath;
38
+ let suppressedLogDepth = 0;
38
39
  export function setLogFile(path) {
39
40
  logFilePath = path;
40
41
  }
42
+ export async function withLogOutputSuppressed(operation) {
43
+ suppressedLogDepth += 1;
44
+ try {
45
+ return await operation();
46
+ }
47
+ finally {
48
+ suppressedLogDepth -= 1;
49
+ }
50
+ }
41
51
  function appendLogLine(line) {
42
52
  if (logFilePath === undefined) {
43
53
  return;
@@ -55,6 +65,9 @@ function appendLogLine(line) {
55
65
  }
56
66
  }
57
67
  export function log(message) {
68
+ if (suppressedLogDepth > 0) {
69
+ return;
70
+ }
58
71
  const timestamp = new Date().toLocaleTimeString();
59
72
  const line = `[${timestamp}] ${message}`;
60
73
  writeOutput(line);
@@ -68,6 +81,9 @@ function formatLogEventFieldValue(value) {
68
81
  return JSON.stringify(raw);
69
82
  }
70
83
  export function logEvent(event, fields) {
84
+ if (suppressedLogDepth > 0) {
85
+ return;
86
+ }
71
87
  const parts = [`event=${formatLogEventFieldValue(event)}`];
72
88
  for (const [key, value] of Object.entries(fields)) {
73
89
  if (value === undefined) {
@@ -103,8 +119,8 @@ export function getLinearClient() {
103
119
  /**
104
120
  * Returns a zero-arg getter that lazily constructs (and caches) a Linear
105
121
  * client on first call. Used by CLI entry points that may not need the
106
- * client at all (e.g. `--no-linear`), so we avoid blowing up on a missing
107
- * API key when no Linear call is actually made. The factory is taken as a
122
+ * client at all, so we avoid blowing up on a missing API key when no Linear
123
+ * call is actually made. The factory is taken as a
108
124
  * parameter (rather than calling `getLinearClient` directly) so callers can
109
125
  * pass their own module-level import of `getLinearClient` — that binding
110
126
  * respects `vi.mock` intercepts, whereas an intra-module reference would
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
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",
@@ -69,7 +69,7 @@
69
69
  "dependencies": {
70
70
  "@clipboard-health/clearance": "1.0.8",
71
71
  "@inquirer/checkbox": "5.1.5",
72
- "@linear/sdk": "85.0.0",
72
+ "@linear/sdk": "86.0.0",
73
73
  "cosmiconfig": "9.0.1",
74
74
  "tslib": "2.8.1",
75
75
  "zod": "4.4.3"
@@ -1,22 +0,0 @@
1
- export interface TicketCheck {
2
- name: string;
3
- status: "ok" | "fail" | "skipped";
4
- detail?: string;
5
- failureSummary?: string;
6
- }
7
- export interface Section {
8
- name: string;
9
- checks: TicketCheck[];
10
- /** When present and `checks` is empty, the section renders as `(skipped — <skipReason>)`. */
11
- skipReason?: string;
12
- }
13
- interface RenderInput {
14
- command: string;
15
- argument: string;
16
- title?: string;
17
- sections: Section[];
18
- verdict: string;
19
- }
20
- export declare function renderTicketCheckResult(input: RenderInput): string[];
21
- export {};
22
- //# sourceMappingURL=ticketCheck.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ticketCheck.d.ts","sourceRoot":"","sources":["../../src/commands/ticketCheck.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,6FAA6F;IAC7F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAqBD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,EAAE,CAMpE"}
@@ -1,23 +0,0 @@
1
- const STATUS_TAG = {
2
- ok: "[ok]",
3
- fail: "[--]",
4
- skipped: "[? ]",
5
- };
6
- function formatCheck(check) {
7
- const tag = STATUS_TAG[check.status];
8
- const detail = check.detail === undefined ? "" : ` (${check.detail})`;
9
- return ` ${tag} ${check.name}${detail}`;
10
- }
11
- function sectionLines(section) {
12
- if (section.checks.length === 0 && section.skipReason !== undefined) {
13
- return [section.name, ` (skipped — ${section.skipReason})`];
14
- }
15
- return [section.name, ...section.checks.map(formatCheck)];
16
- }
17
- export function renderTicketCheckResult(input) {
18
- const titlePart = input.title === undefined ? "" : ` (${input.title})`;
19
- const header = `groundcrew ${input.command} ${input.argument}${titlePart}`;
20
- const bar = "─".repeat(header.length);
21
- const body = input.sections.flatMap((section) => ["", ...sectionLines(section)]);
22
- return [header, bar, ...body, "", input.verdict];
23
- }