@clipboard-health/groundcrew 2.2.0 → 2.3.1
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 +72 -8
- package/configExample.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -10
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +10 -35
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +60 -13
- package/dist/commands/eligibility.d.ts +14 -0
- package/dist/commands/eligibility.d.ts.map +1 -1
- package/dist/commands/eligibility.js +44 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +23 -4
- package/dist/commands/ticketDoctor.d.ts +48 -0
- package/dist/commands/ticketDoctor.d.ts.map +1 -0
- package/dist/commands/ticketDoctor.js +402 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/boardSource.d.ts +55 -0
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +171 -26
- package/dist/lib/config.d.ts +63 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +75 -7
- package/dist/lib/dockerSandbox.d.ts +40 -0
- package/dist/lib/dockerSandbox.d.ts.map +1 -0
- package/dist/lib/dockerSandbox.js +58 -0
- package/dist/lib/host.d.ts +10 -0
- package/dist/lib/host.d.ts.map +1 -1
- package/dist/lib/host.js +8 -3
- package/dist/lib/launchCommand.d.ts +17 -3
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +66 -8
- package/dist/lib/localRunner.d.ts +22 -1
- package/dist/lib/localRunner.d.ts.map +1 -1
- package/dist/lib/localRunner.js +48 -5
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +138 -40
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Blocker, type RawLinearIssue } from "../lib/boardSource.ts";
|
|
2
|
+
import { type ResolvedConfig } from "../lib/config.ts";
|
|
3
|
+
import { type UsageByModel } from "../lib/usage.ts";
|
|
4
|
+
export type TicketDoctorVerdict = {
|
|
5
|
+
kind: "would-dispatch";
|
|
6
|
+
} | {
|
|
7
|
+
kind: "ineligible";
|
|
8
|
+
reason: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "unresolvable";
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export interface TicketCheck {
|
|
14
|
+
name: string;
|
|
15
|
+
status: "ok" | "fail" | "skipped";
|
|
16
|
+
detail?: string;
|
|
17
|
+
failureSummary?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface TicketDoctorResult {
|
|
20
|
+
ticket: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
resolution: TicketCheck[];
|
|
23
|
+
eligibility: TicketCheck[];
|
|
24
|
+
verdict: TicketDoctorVerdict;
|
|
25
|
+
}
|
|
26
|
+
export interface TicketDoctorDependencies {
|
|
27
|
+
config: ResolvedConfig;
|
|
28
|
+
ticket: string;
|
|
29
|
+
/**
|
|
30
|
+
* Injected to keep `ticketDoctor` pure and easy to unit-test. Production
|
|
31
|
+
* callers pass a closure that delegates to `fetchRawLinearIssue` with a
|
|
32
|
+
* real `LinearClient`; tests pass a `vi.fn()` returning a fixture.
|
|
33
|
+
*/
|
|
34
|
+
fetchRawIssue: (input: {
|
|
35
|
+
ticket: string;
|
|
36
|
+
}) => Promise<RawLinearIssue>;
|
|
37
|
+
fetchBlockersFor: (input: {
|
|
38
|
+
ticket: string;
|
|
39
|
+
uuid: string;
|
|
40
|
+
}) => Promise<readonly Blocker[]>;
|
|
41
|
+
fetchUsage: () => Promise<UsageByModel>;
|
|
42
|
+
countInProgress: () => Promise<number>;
|
|
43
|
+
}
|
|
44
|
+
export declare function renderTicketDoctorResult(result: TicketDoctorResult): string[];
|
|
45
|
+
export declare function ticketDoctor(dependencies: TicketDoctorDependencies): Promise<TicketDoctorResult>;
|
|
46
|
+
export declare function ticketDoctorCli(argv: string[]): Promise<void>;
|
|
47
|
+
export declare function runTicketDoctor(ticket: string): Promise<boolean>;
|
|
48
|
+
//# sourceMappingURL=ticketDoctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticketDoctor.d.ts","sourceRoot":"","sources":["../../src/commands/ticketDoctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,OAAO,EAEZ,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAA+B,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACpF,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AASrE,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAC1B;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7C,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,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACtE,gBAAgB,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IAC3F,UAAU,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC;IACxC,eAAe,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACxC;AA+SD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,EAAE,CAmB7E;AAED,wBAAsB,YAAY,CAChC,YAAY,EAAE,wBAAwB,GACrC,OAAO,CAAC,kBAAkB,CAAC,CA0F7B;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAenE;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBtE"}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fetchBlockersForTicket, fetchInProgressIssueCount, fetchRawLinearIssue, resolveModelFor, resolveRepositoryFor, } from "../lib/boardSource.js";
|
|
4
|
+
import { AGENT_ANY_MODEL, loadConfig } from "../lib/config.js";
|
|
5
|
+
import { getUsageByModel } from "../lib/usage.js";
|
|
6
|
+
import { getLinearClient, writeOutput } from "../lib/util.js";
|
|
7
|
+
import { classifyBlockers, classifyUsageExhaustion, pickBestModel, } from "./eligibility.js";
|
|
8
|
+
function buildModelChecks(raw, config) {
|
|
9
|
+
const modelResolution = resolveModelFor({ labels: raw.labels, config });
|
|
10
|
+
const checks = [];
|
|
11
|
+
switch (modelResolution.kind) {
|
|
12
|
+
case "no-label": {
|
|
13
|
+
checks.push({
|
|
14
|
+
name: "Has agent-* label",
|
|
15
|
+
status: "fail",
|
|
16
|
+
detail: "no agent-* label on this ticket",
|
|
17
|
+
failureSummary: "ticket has no agent-* label",
|
|
18
|
+
});
|
|
19
|
+
checks.push({ name: "Model resolves from agent-* label", status: "skipped" });
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case "agent-any": {
|
|
23
|
+
checks.push({
|
|
24
|
+
name: "Has agent-* label",
|
|
25
|
+
status: "ok",
|
|
26
|
+
detail: "agent-any",
|
|
27
|
+
});
|
|
28
|
+
checks.push({
|
|
29
|
+
name: "Model resolves from agent-* label",
|
|
30
|
+
status: "ok",
|
|
31
|
+
detail: `model picked at dispatch time; defaults to "${config.models.default}" when usage ties`,
|
|
32
|
+
});
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "matched": {
|
|
36
|
+
checks.push({
|
|
37
|
+
name: "Has agent-* label",
|
|
38
|
+
status: "ok",
|
|
39
|
+
detail: `agent-${modelResolution.model}`,
|
|
40
|
+
});
|
|
41
|
+
checks.push({
|
|
42
|
+
name: "Model resolves from agent-* label",
|
|
43
|
+
status: "ok",
|
|
44
|
+
detail: `model "${modelResolution.model}"`,
|
|
45
|
+
});
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "disabled-fallback": {
|
|
49
|
+
checks.push({
|
|
50
|
+
name: "Has agent-* label",
|
|
51
|
+
status: "ok",
|
|
52
|
+
detail: `agent-${modelResolution.requestedModel}`,
|
|
53
|
+
});
|
|
54
|
+
checks.push({
|
|
55
|
+
name: "Model resolves from agent-* label",
|
|
56
|
+
status: "ok",
|
|
57
|
+
detail: `agent-${modelResolution.requestedModel} disabled; falling back to model "${modelResolution.fallbackModel}"`,
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
/* v8 ignore next @preserve */
|
|
62
|
+
default: {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let resolvedModel = config.models.default;
|
|
67
|
+
if (modelResolution.kind === "matched") {
|
|
68
|
+
resolvedModel = modelResolution.model;
|
|
69
|
+
}
|
|
70
|
+
else if (modelResolution.kind === "agent-any") {
|
|
71
|
+
resolvedModel = AGENT_ANY_MODEL;
|
|
72
|
+
}
|
|
73
|
+
else if (modelResolution.kind === "disabled-fallback") {
|
|
74
|
+
resolvedModel = modelResolution.fallbackModel;
|
|
75
|
+
}
|
|
76
|
+
return { resolvedModel, checks };
|
|
77
|
+
}
|
|
78
|
+
function buildRepoChecks(raw, config, ticket) {
|
|
79
|
+
const repositoryResolution = resolveRepositoryFor({
|
|
80
|
+
description: raw.description,
|
|
81
|
+
config,
|
|
82
|
+
ticket,
|
|
83
|
+
});
|
|
84
|
+
const checks = [];
|
|
85
|
+
if (repositoryResolution.kind === "ok") {
|
|
86
|
+
checks.push({
|
|
87
|
+
name: "Description mentions known repo",
|
|
88
|
+
status: "ok",
|
|
89
|
+
detail: repositoryResolution.repository,
|
|
90
|
+
});
|
|
91
|
+
const repoDir = join(config.workspace.projectDir, repositoryResolution.repository);
|
|
92
|
+
if (existsSync(repoDir)) {
|
|
93
|
+
checks.push({
|
|
94
|
+
name: "Resolved repo is cloned locally",
|
|
95
|
+
status: "ok",
|
|
96
|
+
detail: repoDir,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
checks.push({
|
|
101
|
+
name: "Resolved repo is cloned locally",
|
|
102
|
+
status: "fail",
|
|
103
|
+
detail: `${repositoryResolution.repository} not found at ${repoDir} — run \`crew setup repos ${repositoryResolution.repository}\``,
|
|
104
|
+
failureSummary: `resolved repo ${repositoryResolution.repository} is not cloned locally`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
checks.push({
|
|
110
|
+
name: "Description mentions known repo",
|
|
111
|
+
status: "fail",
|
|
112
|
+
detail: `no entry from workspace.knownRepositories (${config.workspace.knownRepositories.join(", ")}) appears in description`,
|
|
113
|
+
failureSummary: "description does not mention a known repo",
|
|
114
|
+
});
|
|
115
|
+
checks.push({
|
|
116
|
+
name: "Resolved repo is cloned locally",
|
|
117
|
+
status: "skipped",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// repositoryResolution.kind is "ok" only when the first check passed.
|
|
121
|
+
/* v8 ignore else @preserve */
|
|
122
|
+
const resolvedRepository = repositoryResolution.kind === "ok" ? repositoryResolution.repository : "";
|
|
123
|
+
return { resolvedRepository, checks };
|
|
124
|
+
}
|
|
125
|
+
async function runEligibilityChecks(arguments_) {
|
|
126
|
+
const { ticket, raw, config, resolvedRepository, resolvedModel, dependencies, eligibility } = arguments_;
|
|
127
|
+
const blockers = await dependencies.fetchBlockersFor({ ticket, uuid: raw.uuid });
|
|
128
|
+
const groundcrewIssue = {
|
|
129
|
+
id: ticket,
|
|
130
|
+
uuid: raw.uuid,
|
|
131
|
+
title: raw.title,
|
|
132
|
+
status: raw.stateName,
|
|
133
|
+
statusId: "",
|
|
134
|
+
assignee: "",
|
|
135
|
+
updatedAt: "",
|
|
136
|
+
teamId: raw.teamId,
|
|
137
|
+
repository: resolvedRepository,
|
|
138
|
+
model: resolvedModel,
|
|
139
|
+
blockers: [...blockers],
|
|
140
|
+
hasMoreBlockers: raw.hasMoreBlockers,
|
|
141
|
+
};
|
|
142
|
+
const blockerClassification = classifyBlockers(config, [groundcrewIssue]);
|
|
143
|
+
const [firstSkip] = blockerClassification.skips;
|
|
144
|
+
if (firstSkip !== undefined) {
|
|
145
|
+
if (firstSkip.eventReason === "blockers_paginated") {
|
|
146
|
+
eligibility.push({
|
|
147
|
+
name: "No active blockers",
|
|
148
|
+
status: "fail",
|
|
149
|
+
detail: "blockers exceeded the v1 relation page size",
|
|
150
|
+
failureSummary: "blockers exceeded the v1 relation page size",
|
|
151
|
+
});
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
// firstSkip.blockers is always set for "blocked" and "blockers_paginated" skip reasons.
|
|
155
|
+
/* v8 ignore next @preserve */
|
|
156
|
+
const blockerIds = firstSkip.blockers ?? [];
|
|
157
|
+
eligibility.push({
|
|
158
|
+
name: "No active blockers",
|
|
159
|
+
status: "fail",
|
|
160
|
+
detail: blockerIds.join(", "),
|
|
161
|
+
failureSummary: `blocked by ${blockerIds.join(", ")}`,
|
|
162
|
+
});
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
eligibility.push({ name: "No active blockers", status: "ok" });
|
|
166
|
+
const usage = await dependencies.fetchUsage();
|
|
167
|
+
const usageExhaustion = classifyUsageExhaustion(config, usage);
|
|
168
|
+
const exhausted = new Set(usageExhaustion.map((exhaustion) => exhaustion.model));
|
|
169
|
+
let model = resolvedModel;
|
|
170
|
+
let resolvedFromAny = "";
|
|
171
|
+
if (model === AGENT_ANY_MODEL) {
|
|
172
|
+
const picked = pickBestModel(config, usage, exhausted);
|
|
173
|
+
if (picked === undefined) {
|
|
174
|
+
eligibility.push({
|
|
175
|
+
name: "Model usage under sessionLimitPercentage",
|
|
176
|
+
status: "fail",
|
|
177
|
+
detail: "agent-any but no model has available capacity",
|
|
178
|
+
failureSummary: "agent-any has no model with available capacity",
|
|
179
|
+
});
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
model = picked;
|
|
183
|
+
resolvedFromAny = `; agent-any resolved to model "${picked}"`;
|
|
184
|
+
}
|
|
185
|
+
const exhaustedUsage = usageExhaustion.find((exhaustion) => exhaustion.model === model);
|
|
186
|
+
eligibility.push(exhaustedUsage === undefined
|
|
187
|
+
? modelUsageOkCheck({ config, model, usage, resolvedFromAny })
|
|
188
|
+
: usageExhaustionCheck(exhaustedUsage));
|
|
189
|
+
const inProgress = await dependencies.countInProgress();
|
|
190
|
+
const cap = config.orchestrator.maximumInProgress;
|
|
191
|
+
const capOk = inProgress < cap;
|
|
192
|
+
const capCheck = {
|
|
193
|
+
name: "In-progress cap not hit",
|
|
194
|
+
status: capOk ? "ok" : "fail",
|
|
195
|
+
detail: `${inProgress}/${cap} used`,
|
|
196
|
+
};
|
|
197
|
+
if (!capOk) {
|
|
198
|
+
capCheck.failureSummary = `in-progress cap is full (${inProgress}/${cap} used)`;
|
|
199
|
+
}
|
|
200
|
+
eligibility.push(capCheck);
|
|
201
|
+
return eligibility.every((check) => check.status === "ok");
|
|
202
|
+
}
|
|
203
|
+
function modelUsageOkCheck(arguments_) {
|
|
204
|
+
const { config, model, usage, resolvedFromAny } = arguments_;
|
|
205
|
+
const sessionPercent = ((usage[model]?.session ?? 0) * 100).toFixed(0);
|
|
206
|
+
return {
|
|
207
|
+
name: `Model "${model}" usage under sessionLimitPercentage`,
|
|
208
|
+
status: "ok",
|
|
209
|
+
detail: `${sessionPercent}% (limit ${config.orchestrator.sessionLimitPercentage}%)${resolvedFromAny}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function usageExhaustionCheck(exhaustion) {
|
|
213
|
+
if (exhaustion.kind === "session") {
|
|
214
|
+
return {
|
|
215
|
+
name: `Model "${exhaustion.model}" usage under sessionLimitPercentage`,
|
|
216
|
+
status: "fail",
|
|
217
|
+
detail: `${exhaustion.usedPercentage.toFixed(0)}% (limit ${exhaustion.limitPercentage}%)`,
|
|
218
|
+
failureSummary: `${exhaustion.model} session usage ${exhaustion.usedPercentage.toFixed(0)}% over ${exhaustion.limitPercentage}% limit`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
name: `Model "${exhaustion.model}" weekly usage within paced budget`,
|
|
223
|
+
status: "fail",
|
|
224
|
+
detail: `${exhaustion.usedPercentage.toFixed(1)}% (paced budget ${exhaustion.allowedPercentage.toFixed(1)}%, resets in ${exhaustion.resetMinutes}m)`,
|
|
225
|
+
failureSummary: `${exhaustion.model} weekly usage ${exhaustion.usedPercentage.toFixed(1)}% over ${exhaustion.allowedPercentage.toFixed(1)}% paced budget`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const STATUS_TAG = {
|
|
229
|
+
ok: "[ok]",
|
|
230
|
+
fail: "[--]",
|
|
231
|
+
skipped: "[? ]",
|
|
232
|
+
};
|
|
233
|
+
function formatCheck(check) {
|
|
234
|
+
const tag = STATUS_TAG[check.status];
|
|
235
|
+
const detail = check.detail === undefined ? "" : ` (${check.detail})`;
|
|
236
|
+
return ` ${tag} ${check.name}${detail}`;
|
|
237
|
+
}
|
|
238
|
+
function formatVerdict(verdict) {
|
|
239
|
+
switch (verdict.kind) {
|
|
240
|
+
case "would-dispatch": {
|
|
241
|
+
return "→ would be dispatched on next tick";
|
|
242
|
+
}
|
|
243
|
+
case "unresolvable": {
|
|
244
|
+
return `→ unresolvable: ${verdict.reason}`;
|
|
245
|
+
}
|
|
246
|
+
case "ineligible": {
|
|
247
|
+
return `→ ineligible: ${verdict.reason}`;
|
|
248
|
+
}
|
|
249
|
+
/* v8 ignore next 3 @preserve -- exhaustive over TicketDoctorVerdict.kind */
|
|
250
|
+
default: {
|
|
251
|
+
return `→ ${verdict.kind}`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function eligibilityLines(result) {
|
|
256
|
+
if (result.eligibility.length === 0) {
|
|
257
|
+
const skipMessage = result.verdict.kind === "unresolvable"
|
|
258
|
+
? " (skipped — ticket unresolved)"
|
|
259
|
+
: " (skipped — resolution checks failed)";
|
|
260
|
+
return [skipMessage];
|
|
261
|
+
}
|
|
262
|
+
return result.eligibility.map(formatCheck);
|
|
263
|
+
}
|
|
264
|
+
export function renderTicketDoctorResult(result) {
|
|
265
|
+
const titlePart = result.title === undefined ? "" : ` (${result.title})`;
|
|
266
|
+
const header = `groundcrew doctor --ticket ${result.ticket}${titlePart}`;
|
|
267
|
+
const bar = "─".repeat(header.length);
|
|
268
|
+
const verdictLine = formatVerdict(result.verdict);
|
|
269
|
+
return [
|
|
270
|
+
header,
|
|
271
|
+
bar,
|
|
272
|
+
"",
|
|
273
|
+
"Resolution",
|
|
274
|
+
...result.resolution.map(formatCheck),
|
|
275
|
+
"",
|
|
276
|
+
"Eligibility",
|
|
277
|
+
...eligibilityLines(result),
|
|
278
|
+
"",
|
|
279
|
+
verdictLine,
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
export async function ticketDoctor(dependencies) {
|
|
283
|
+
const ticket = dependencies.ticket.toUpperCase();
|
|
284
|
+
const resolution = [];
|
|
285
|
+
const eligibility = [];
|
|
286
|
+
let raw;
|
|
287
|
+
try {
|
|
288
|
+
raw = await dependencies.fetchRawIssue({ ticket });
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
292
|
+
resolution.push({ name: "Ticket exists in Linear", status: "fail", detail: message });
|
|
293
|
+
return {
|
|
294
|
+
ticket,
|
|
295
|
+
resolution,
|
|
296
|
+
eligibility,
|
|
297
|
+
verdict: { kind: "unresolvable", reason: message },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const { config } = dependencies;
|
|
301
|
+
resolution.push({ name: "Ticket exists in Linear", status: "ok", detail: `"${raw.title}"` });
|
|
302
|
+
// Status check
|
|
303
|
+
const todoState = config.linear.statuses.todo;
|
|
304
|
+
if (raw.stateName === todoState) {
|
|
305
|
+
resolution.push({ name: "Status is Todo", status: "ok" });
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
resolution.push({
|
|
309
|
+
name: "Status is Todo",
|
|
310
|
+
status: "fail",
|
|
311
|
+
detail: `current: ${raw.stateName}`,
|
|
312
|
+
failureSummary: `status is ${raw.stateName} (need ${todoState})`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// Label + model checks
|
|
316
|
+
const { resolvedModel, checks: modelChecks } = buildModelChecks(raw, config);
|
|
317
|
+
resolution.push(...modelChecks);
|
|
318
|
+
// Repo checks
|
|
319
|
+
const { resolvedRepository, checks: repoChecks } = buildRepoChecks(raw, config, ticket);
|
|
320
|
+
resolution.push(...repoChecks);
|
|
321
|
+
const firstResolutionFail = resolution.find((check) => check.status === "fail");
|
|
322
|
+
if (firstResolutionFail !== undefined) {
|
|
323
|
+
// failureSummary is always set for all resolution fail paths; .name fallback is defensive.
|
|
324
|
+
/* v8 ignore next @preserve */
|
|
325
|
+
const resolutionReason = firstResolutionFail.failureSummary ?? firstResolutionFail.name;
|
|
326
|
+
return {
|
|
327
|
+
ticket,
|
|
328
|
+
title: raw.title,
|
|
329
|
+
resolution,
|
|
330
|
+
eligibility,
|
|
331
|
+
verdict: { kind: "ineligible", reason: resolutionReason },
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// All resolution checks passed (or were skipped). Run eligibility checks.
|
|
335
|
+
const allEligibilityOk = await runEligibilityChecks({
|
|
336
|
+
ticket,
|
|
337
|
+
raw,
|
|
338
|
+
config,
|
|
339
|
+
resolvedRepository,
|
|
340
|
+
resolvedModel,
|
|
341
|
+
dependencies,
|
|
342
|
+
eligibility,
|
|
343
|
+
});
|
|
344
|
+
if (!allEligibilityOk) {
|
|
345
|
+
const firstEligibilityFail = eligibility.find((check) => check.status === "fail");
|
|
346
|
+
// firstEligibilityFail is always defined when allEligibilityOk is false; fallback is defensive.
|
|
347
|
+
/* v8 ignore next @preserve */
|
|
348
|
+
const reason = firstEligibilityFail?.failureSummary ??
|
|
349
|
+
firstEligibilityFail?.name ??
|
|
350
|
+
"eligibility check failed";
|
|
351
|
+
return {
|
|
352
|
+
ticket,
|
|
353
|
+
title: raw.title,
|
|
354
|
+
resolution,
|
|
355
|
+
eligibility,
|
|
356
|
+
verdict: { kind: "ineligible", reason },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
ticket,
|
|
361
|
+
title: raw.title,
|
|
362
|
+
resolution,
|
|
363
|
+
eligibility,
|
|
364
|
+
verdict: { kind: "would-dispatch" },
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
export async function ticketDoctorCli(argv) {
|
|
368
|
+
const [ticket, ...extraArgs] = argv;
|
|
369
|
+
if (ticket === undefined || ticket.length === 0 || ticket.startsWith("-")) {
|
|
370
|
+
throw new Error("Usage: crew doctor --ticket <ticket>");
|
|
371
|
+
}
|
|
372
|
+
/* v8 ignore else @preserve */
|
|
373
|
+
if (extraArgs.length > 0) {
|
|
374
|
+
throw new Error(`crew doctor --ticket: unexpected arguments: ${extraArgs.join(" ")}`);
|
|
375
|
+
}
|
|
376
|
+
/* v8 ignore start @preserve */
|
|
377
|
+
const ok = await runTicketDoctor(ticket);
|
|
378
|
+
if (!ok) {
|
|
379
|
+
process.exitCode = 1;
|
|
380
|
+
}
|
|
381
|
+
/* v8 ignore stop @preserve */
|
|
382
|
+
}
|
|
383
|
+
export async function runTicketDoctor(ticket) {
|
|
384
|
+
const config = await loadConfig();
|
|
385
|
+
let client;
|
|
386
|
+
const linearClient = () => {
|
|
387
|
+
client ??= getLinearClient();
|
|
388
|
+
return client;
|
|
389
|
+
};
|
|
390
|
+
const result = await ticketDoctor({
|
|
391
|
+
config,
|
|
392
|
+
ticket,
|
|
393
|
+
fetchRawIssue: async ({ ticket: t }) => await fetchRawLinearIssue({ client: linearClient(), ticket: t }),
|
|
394
|
+
fetchBlockersFor: async ({ ticket: t, uuid }) => await fetchBlockersForTicket({ client: linearClient(), ticket: t, uuid }),
|
|
395
|
+
fetchUsage: async () => await getUsageByModel(config),
|
|
396
|
+
countInProgress: async () => await fetchInProgressIssueCount({ client: linearClient(), config }),
|
|
397
|
+
});
|
|
398
|
+
for (const line of renderTicketDoctorResult(result)) {
|
|
399
|
+
writeOutput(line);
|
|
400
|
+
}
|
|
401
|
+
return result.verdict.kind === "would-dispatch";
|
|
402
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,5 +5,7 @@ export { orchestrate, type OrchestratorOptions } from "./commands/orchestrator.t
|
|
|
5
5
|
export { setupWorkspace, type SetupWorkspaceOptions } from "./commands/setupWorkspace.ts";
|
|
6
6
|
export type { Config, ModelDefinition, ResolvedConfig } from "./lib/config.ts";
|
|
7
7
|
export { loadConfig } from "./lib/config.ts";
|
|
8
|
+
export { fetchBlockersForTicket, fetchInProgressIssueCount, fetchRawLinearIssue, fetchResolvedIssue, resolveModelFor, resolveRepositoryFor, type ModelResolution, type RawLinearIssue, type RepositoryResolution, } from "./lib/boardSource.ts";
|
|
8
9
|
export { getUsageByModel, type UsageByModel } from "./lib/usage.ts";
|
|
10
|
+
export { ticketDoctor, type TicketCheck, type TicketDoctorDependencies, type TicketDoctorResult, type TicketDoctorVerdict, } from "./commands/ticketDoctor.ts";
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,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,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,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,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,6 @@ export { doctor } from "./commands/doctor.js";
|
|
|
4
4
|
export { orchestrate } from "./commands/orchestrator.js";
|
|
5
5
|
export { setupWorkspace } from "./commands/setupWorkspace.js";
|
|
6
6
|
export { loadConfig } from "./lib/config.js";
|
|
7
|
+
export { fetchBlockersForTicket, fetchInProgressIssueCount, fetchRawLinearIssue, fetchResolvedIssue, resolveModelFor, resolveRepositoryFor, } from "./lib/boardSource.js";
|
|
7
8
|
export { getUsageByModel } from "./lib/usage.js";
|
|
9
|
+
export { ticketDoctor, } from "./commands/ticketDoctor.js";
|
|
@@ -69,6 +69,61 @@ interface ResolvedIssue {
|
|
|
69
69
|
model: string;
|
|
70
70
|
teamId: string;
|
|
71
71
|
}
|
|
72
|
+
export interface RawLinearIssue {
|
|
73
|
+
uuid: string;
|
|
74
|
+
title: string;
|
|
75
|
+
description: string;
|
|
76
|
+
teamId: string;
|
|
77
|
+
labels: {
|
|
78
|
+
name: string;
|
|
79
|
+
}[];
|
|
80
|
+
/** Linear workflow state name, e.g. "Todo", "In Review". May be "" if state was null. */
|
|
81
|
+
stateName: string;
|
|
82
|
+
blockers: Blocker[];
|
|
83
|
+
hasMoreBlockers: boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare function fetchBlockersForTicket(arguments_: {
|
|
86
|
+
client: LinearClient;
|
|
87
|
+
ticket: string;
|
|
88
|
+
uuid: string;
|
|
89
|
+
}): Promise<readonly Blocker[]>;
|
|
90
|
+
export declare function fetchRawLinearIssue(arguments_: {
|
|
91
|
+
client: LinearClient;
|
|
92
|
+
ticket: string;
|
|
93
|
+
}): Promise<RawLinearIssue>;
|
|
94
|
+
export declare function fetchInProgressIssueCount(arguments_: {
|
|
95
|
+
client: LinearClient;
|
|
96
|
+
config: ResolvedConfig;
|
|
97
|
+
}): Promise<number>;
|
|
98
|
+
export type RepositoryResolution = {
|
|
99
|
+
kind: "ok";
|
|
100
|
+
repository: string;
|
|
101
|
+
} | {
|
|
102
|
+
kind: "missing";
|
|
103
|
+
};
|
|
104
|
+
export declare function resolveRepositoryFor(arguments_: {
|
|
105
|
+
description: string | undefined;
|
|
106
|
+
config: ResolvedConfig;
|
|
107
|
+
ticket: string;
|
|
108
|
+
}): RepositoryResolution;
|
|
109
|
+
export type ModelResolution = {
|
|
110
|
+
kind: "matched";
|
|
111
|
+
model: string;
|
|
112
|
+
} | {
|
|
113
|
+
kind: "no-label";
|
|
114
|
+
} | {
|
|
115
|
+
kind: "agent-any";
|
|
116
|
+
} | {
|
|
117
|
+
kind: "disabled-fallback";
|
|
118
|
+
requestedModel: string;
|
|
119
|
+
fallbackModel: string;
|
|
120
|
+
};
|
|
121
|
+
export declare function resolveModelFor(arguments_: {
|
|
122
|
+
labels: {
|
|
123
|
+
name: string;
|
|
124
|
+
}[];
|
|
125
|
+
config: ResolvedConfig;
|
|
126
|
+
}): ModelResolution;
|
|
72
127
|
/**
|
|
73
128
|
* `agent-any` collapses to `models.default` here — manual setup doesn't run
|
|
74
129
|
* the usage-gated `any` resolver, so the caller gets a concrete model name
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boardSource.d.ts","sourceRoot":"","sources":["../../src/lib/boardSource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7F,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,0FAA0F;IAC1F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAExE;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,YAAmB,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAMjF;CACF;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAUpE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAEhF;
|
|
1
|
+
{"version":3,"file":"boardSource.d.ts","sourceRoot":"","sources":["../../src/lib/boardSource.ts"],"names":[],"mappings":"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"}
|