@clipboard-health/groundcrew 3.1.2 → 3.1.4
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 +47 -17
- package/crew.config.example.ts +25 -10
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -0
- package/dist/commands/cleaner.d.ts.map +1 -1
- package/dist/commands/cleaner.js +6 -4
- package/dist/commands/cleanupWorkspace.d.ts.map +1 -1
- package/dist/commands/cleanupWorkspace.js +2 -0
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +6 -6
- package/dist/commands/eligibility.d.ts.map +1 -1
- package/dist/commands/eligibility.js +2 -2
- package/dist/commands/interruptWorkspace.d.ts +8 -0
- package/dist/commands/interruptWorkspace.d.ts.map +1 -0
- package/dist/commands/interruptWorkspace.js +108 -0
- package/dist/commands/orchestrator.d.ts +4 -2
- package/dist/commands/orchestrator.d.ts.map +1 -1
- package/dist/commands/orchestrator.js +6 -105
- package/dist/commands/resumeWorkspace.d.ts +7 -0
- package/dist/commands/resumeWorkspace.d.ts.map +1 -0
- package/dist/commands/resumeWorkspace.js +163 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +78 -79
- package/dist/commands/ticketDoctor.d.ts +18 -3
- package/dist/commands/ticketDoctor.d.ts.map +1 -1
- package/dist/commands/ticketDoctor.js +105 -11
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/lib/agentLaunch.d.ts +29 -0
- package/dist/lib/agentLaunch.d.ts.map +1 -0
- package/dist/lib/agentLaunch.js +53 -0
- package/dist/lib/boardSource.d.ts +41 -5
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +211 -70
- package/dist/lib/config.d.ts +59 -25
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +130 -22
- package/dist/lib/linearIssueStatus.d.ts +3 -1
- package/dist/lib/linearIssueStatus.d.ts.map +1 -1
- package/dist/lib/linearIssueStatus.js +0 -0
- package/dist/lib/runState.d.ts +46 -0
- package/dist/lib/runState.d.ts.map +1 -0
- package/dist/lib/runState.js +137 -0
- package/dist/lib/runStateCleanup.d.ts +4 -0
- package/dist/lib/runStateCleanup.d.ts.map +1 -0
- package/dist/lib/runStateCleanup.js +12 -0
- package/dist/lib/stagedLaunch.d.ts +32 -0
- package/dist/lib/stagedLaunch.d.ts.map +1 -0
- package/dist/lib/stagedLaunch.js +58 -0
- package/dist/lib/util.d.ts +0 -1
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +0 -4
- package/dist/lib/workspaces.d.ts +19 -1
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +29 -9
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +12 -4
- package/package.json +1 -1
|
@@ -1,36 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* groundcrew orchestrator — polls
|
|
3
|
-
*
|
|
2
|
+
* groundcrew orchestrator — polls Linear projects and spins up workspace +
|
|
3
|
+
* git-worktree pairs for ready tickets. Each tick fetches the board, runs
|
|
4
|
+
* the cleaner, and runs the dispatcher; logging from those modules is the
|
|
5
|
+
* orchestrator's user-facing output.
|
|
4
6
|
*/
|
|
5
|
-
import { createBoardSource,
|
|
7
|
+
import { createBoardSource, RepositoryResolutionError, } from "../lib/boardSource.js";
|
|
6
8
|
import { loadConfig } from "../lib/config.js";
|
|
7
9
|
import { getUsageByModel } from "../lib/usage.js";
|
|
8
|
-
import {
|
|
10
|
+
import { errorMessage, getLinearClient, log, sleep } from "../lib/util.js";
|
|
9
11
|
import { worktrees } from "../lib/worktrees.js";
|
|
10
12
|
import { createCleaner } from "./cleaner.js";
|
|
11
13
|
import { createDispatcher } from "./dispatcher.js";
|
|
12
14
|
const RATE_LIMIT_DELAY_MS = 60_000;
|
|
13
15
|
const RETRY_BASE_DELAY_MS = 1000;
|
|
14
16
|
const RETRY_MAX_ATTEMPTS = 3;
|
|
15
|
-
const STATUS_CARD_TITLE_WIDTH = 42;
|
|
16
|
-
const STATUS_CARD_ID_WIDTH = 8;
|
|
17
|
-
const STATUS_CARD_LIMIT = 10;
|
|
18
|
-
const HEADER_BAR_WIDTH = 70;
|
|
19
|
-
const SECTION_BAR_WIDTH = 50;
|
|
20
17
|
const MS_PER_SECOND = 1000;
|
|
21
|
-
const STATUS_ICON_DEFAULT = " ";
|
|
22
|
-
function statusIconFor(status, config) {
|
|
23
|
-
if (status === config.linear.statuses.inProgress) {
|
|
24
|
-
return ">>";
|
|
25
|
-
}
|
|
26
|
-
if (status === config.linear.statuses.todo) {
|
|
27
|
-
return "--";
|
|
28
|
-
}
|
|
29
|
-
if (isTerminalStatus(status, config)) {
|
|
30
|
-
return "ok";
|
|
31
|
-
}
|
|
32
|
-
return STATUS_ICON_DEFAULT;
|
|
33
|
-
}
|
|
34
18
|
async function withRetry(function_, signal, maxRetries = RETRY_MAX_ATTEMPTS, baseDelayMs = RETRY_BASE_DELAY_MS) {
|
|
35
19
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
36
20
|
try {
|
|
@@ -64,86 +48,6 @@ class WatchLoopShutdownError extends Error {
|
|
|
64
48
|
this.name = "WatchLoopShutdownError";
|
|
65
49
|
}
|
|
66
50
|
}
|
|
67
|
-
function groupByStatus(issues, knownOrder) {
|
|
68
|
-
const groups = new Map();
|
|
69
|
-
for (const status of knownOrder) {
|
|
70
|
-
groups.set(status, []);
|
|
71
|
-
}
|
|
72
|
-
for (const issue of issues) {
|
|
73
|
-
/* v8 ignore next @preserve -- knownOrder seeds an entry for each issue.status returned by buildStatusOrder */
|
|
74
|
-
const group = groups.get(issue.status) ?? [];
|
|
75
|
-
group.push(issue);
|
|
76
|
-
groups.set(issue.status, group);
|
|
77
|
-
}
|
|
78
|
-
return groups;
|
|
79
|
-
}
|
|
80
|
-
function buildStatusOrder(state, config) {
|
|
81
|
-
const head = [
|
|
82
|
-
...new Set([
|
|
83
|
-
config.linear.statuses.inProgress,
|
|
84
|
-
config.linear.statuses.todo,
|
|
85
|
-
config.linear.statuses.done,
|
|
86
|
-
...config.linear.statuses.terminal,
|
|
87
|
-
]),
|
|
88
|
-
];
|
|
89
|
-
const seen = new Set(head);
|
|
90
|
-
const tail = [];
|
|
91
|
-
for (const issue of state.issues) {
|
|
92
|
-
if (!seen.has(issue.status)) {
|
|
93
|
-
seen.add(issue.status);
|
|
94
|
-
tail.push(issue.status);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return [...head, ...tail];
|
|
98
|
-
}
|
|
99
|
-
function render(state, config, previous) {
|
|
100
|
-
const order = buildStatusOrder(state, config);
|
|
101
|
-
const grouped = groupByStatus(state.issues, order);
|
|
102
|
-
const previousGrouped = previous ? groupByStatus(previous.issues, order) : undefined;
|
|
103
|
-
const previousById = previous
|
|
104
|
-
? new Map(previous.issues.map((issue) => [issue.id, issue]))
|
|
105
|
-
: undefined;
|
|
106
|
-
clearOutput();
|
|
107
|
-
writeOutput(`groundcrew — ${config.linear.projectSlug} — ${new Date(state.timestamp).toLocaleTimeString()}`);
|
|
108
|
-
writeOutput(`Max in progress: ${config.orchestrator.maximumInProgress}`);
|
|
109
|
-
writeOutput("=".repeat(HEADER_BAR_WIDTH));
|
|
110
|
-
writeOutput();
|
|
111
|
-
for (const [status, issues] of grouped) {
|
|
112
|
-
if (issues.length === 0) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const previousCount = previousGrouped?.get(status)?.length ?? issues.length;
|
|
116
|
-
const delta = issues.length === previousCount
|
|
117
|
-
? ""
|
|
118
|
-
: ` (${issues.length > previousCount ? "+" : ""}${issues.length - previousCount})`;
|
|
119
|
-
writeOutput(`${statusIconFor(status, config)} ${status} (${issues.length})${delta}`);
|
|
120
|
-
writeOutput("-".repeat(SECTION_BAR_WIDTH));
|
|
121
|
-
// Cap each status at the N most recent so a backlog of hundreds of Done
|
|
122
|
-
// tickets doesn't clog the terminal. Sort only when truncating so smaller
|
|
123
|
-
// statuses keep whatever order Linear returned.
|
|
124
|
-
const visible = issues.length > STATUS_CARD_LIMIT
|
|
125
|
-
? issues
|
|
126
|
-
.toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
127
|
-
.slice(0, STATUS_CARD_LIMIT)
|
|
128
|
-
: issues;
|
|
129
|
-
for (const issue of visible) {
|
|
130
|
-
const previousIssue = previousById?.get(issue.id);
|
|
131
|
-
const changed = previousIssue && previousIssue.status !== issue.status
|
|
132
|
-
? ` [was: ${previousIssue.status}]`
|
|
133
|
-
: "";
|
|
134
|
-
writeOutput(` ${issue.id.padEnd(STATUS_CARD_ID_WIDTH)} ${issue.title.slice(0, STATUS_CARD_TITLE_WIDTH).padEnd(STATUS_CARD_TITLE_WIDTH)} ${issue.assignee}${changed}`);
|
|
135
|
-
}
|
|
136
|
-
if (issues.length > STATUS_CARD_LIMIT) {
|
|
137
|
-
writeOutput(` … showing ${STATUS_CARD_LIMIT} most recent of ${issues.length}; ${issues.length - STATUS_CARD_LIMIT} older hidden`);
|
|
138
|
-
}
|
|
139
|
-
writeOutput();
|
|
140
|
-
}
|
|
141
|
-
const total = state.issues.length;
|
|
142
|
-
const done = state.issues.filter((issue) => isTerminalStatus(issue.status, config)).length;
|
|
143
|
-
/* v8 ignore next @preserve -- grouped has all known statuses pre-seeded by groupByStatus */
|
|
144
|
-
const active = grouped.get(config.linear.statuses.inProgress)?.length ?? 0;
|
|
145
|
-
writeOutput(`Total: ${total} | Active: ${active}/${config.orchestrator.maximumInProgress} | Done: ${done} | Remaining: ${total - done}`);
|
|
146
|
-
}
|
|
147
51
|
async function fetchUsageOrEmpty(config, signal) {
|
|
148
52
|
try {
|
|
149
53
|
return await getUsageByModel(config, signal);
|
|
@@ -163,10 +67,8 @@ export async function orchestrate(options) {
|
|
|
163
67
|
await boardSource.verify();
|
|
164
68
|
const cleaner = createCleaner({ config });
|
|
165
69
|
const dispatcher = createDispatcher({ config, client });
|
|
166
|
-
let previous;
|
|
167
70
|
const tick = async (signal) => {
|
|
168
71
|
const state = await withRetry(async () => await boardSource.fetch(), signal);
|
|
169
|
-
render(state, config, previous);
|
|
170
72
|
const worktreeEntries = worktrees.list(config);
|
|
171
73
|
const tickArguments = {
|
|
172
74
|
state,
|
|
@@ -181,7 +83,6 @@ export async function orchestrate(options) {
|
|
|
181
83
|
// an idle board doesn't burn a codexbar shell-out per tick.
|
|
182
84
|
usage: async (usageSignal) => await fetchUsageOrEmpty(config, usageSignal),
|
|
183
85
|
});
|
|
184
|
-
previous = state;
|
|
185
86
|
};
|
|
186
87
|
await (options.watch ? runWatchLoop(tick, config) : tick());
|
|
187
88
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ResolvedConfig } from "../lib/config.ts";
|
|
2
|
+
export interface ResumeWorkspaceOptions {
|
|
3
|
+
ticket: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function resumeWorkspace(config: ResolvedConfig, options: ResumeWorkspaceOptions): Promise<void>;
|
|
6
|
+
export declare function resumeWorkspaceCli(argv: string[]): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=resumeWorkspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;CAChB;AA6HD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Df;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { fetchResolvedIssue } from "../lib/boardSource.js";
|
|
2
|
+
import { loadConfig } from "../lib/config.js";
|
|
3
|
+
import { ensureAgentSandbox, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
4
|
+
import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
5
|
+
import { readRunState, recordRunState } from "../lib/runState.js";
|
|
6
|
+
import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
7
|
+
import { errorMessage, getLinearClient, log } from "../lib/util.js";
|
|
8
|
+
import { workspaces } from "../lib/workspaces.js";
|
|
9
|
+
import { worktrees } from "../lib/worktrees.js";
|
|
10
|
+
function parseArguments(argv) {
|
|
11
|
+
const [ticket, ...extras] = argv;
|
|
12
|
+
if (ticket === undefined || ticket.length === 0 || extras.length > 0 || ticket.startsWith("-")) {
|
|
13
|
+
throw new Error("Usage: crew resume <ticket>");
|
|
14
|
+
}
|
|
15
|
+
return { ticket: ticket.toLowerCase() };
|
|
16
|
+
}
|
|
17
|
+
async function fetchTicketDetails(ticket) {
|
|
18
|
+
try {
|
|
19
|
+
const issue = await getLinearClient().issue(ticket.toUpperCase());
|
|
20
|
+
return {
|
|
21
|
+
title: issue.title,
|
|
22
|
+
description: issue.description ?? "",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
log(`Resume Linear detail lookup failed for ${ticket}: ${errorMessage(error)}`);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function contextFromLinear(config, ticket, worktree) {
|
|
31
|
+
const resolved = await fetchResolvedIssue({ client: getLinearClient(), config, ticket });
|
|
32
|
+
return {
|
|
33
|
+
ticket,
|
|
34
|
+
repository: resolved.repository,
|
|
35
|
+
model: resolved.model,
|
|
36
|
+
worktree,
|
|
37
|
+
title: resolved.title,
|
|
38
|
+
description: resolved.description,
|
|
39
|
+
resumeCount: 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function contextFromState(ticket, state, worktree) {
|
|
43
|
+
const details = await fetchTicketDetails(ticket);
|
|
44
|
+
return {
|
|
45
|
+
ticket,
|
|
46
|
+
repository: state.repository,
|
|
47
|
+
model: state.model,
|
|
48
|
+
worktree,
|
|
49
|
+
title: details?.title ?? ticket.toUpperCase(),
|
|
50
|
+
description: details?.description ?? "",
|
|
51
|
+
...(state.reason === undefined ? {} : { reason: state.reason }),
|
|
52
|
+
resumeCount: state.resumeCount,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function buildResumeContext(config, ticket) {
|
|
56
|
+
const state = readRunState(config, ticket);
|
|
57
|
+
const entries = worktrees.findByTicket(config, ticket);
|
|
58
|
+
const worktree = state === undefined
|
|
59
|
+
? entries[0]
|
|
60
|
+
: (entries.find((entry) => entry.repository === state.repository) ?? entries[0]);
|
|
61
|
+
if (worktree === undefined) {
|
|
62
|
+
throw new Error(`No worktree found for ${ticket}; cannot resume.`);
|
|
63
|
+
}
|
|
64
|
+
if (state !== undefined) {
|
|
65
|
+
return await contextFromState(ticket, state, worktree);
|
|
66
|
+
}
|
|
67
|
+
return await contextFromLinear(config, ticket, worktree);
|
|
68
|
+
}
|
|
69
|
+
function renderResumePrompt(context) {
|
|
70
|
+
return [
|
|
71
|
+
`You are resuming Groundcrew ticket ${context.ticket} (${context.title}) in an existing worktree.`,
|
|
72
|
+
"",
|
|
73
|
+
"Ticket description:",
|
|
74
|
+
"",
|
|
75
|
+
context.description,
|
|
76
|
+
"",
|
|
77
|
+
"## Continuation context",
|
|
78
|
+
"",
|
|
79
|
+
`- Worktree: ${context.worktree.dir}`,
|
|
80
|
+
`- Branch: ${context.worktree.branchName}`,
|
|
81
|
+
context.reason === undefined
|
|
82
|
+
? "- Previous interrupt reason: none recorded"
|
|
83
|
+
: `- Previous interrupt reason: ${context.reason}`,
|
|
84
|
+
"",
|
|
85
|
+
"Before editing, inspect the current git status and diff. Continue from the work already present in this worktree; do not restart from scratch unless the diff proves that is necessary.",
|
|
86
|
+
"",
|
|
87
|
+
"Run the repository's documented verification before stopping, then leave the branch ready or open a PR when possible.",
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
async function failIfWorkspaceAlreadyLive(config, ticket) {
|
|
91
|
+
const probe = await workspaces.probe(config);
|
|
92
|
+
if (probe.kind === "unavailable") {
|
|
93
|
+
const detail = probe.error === undefined ? "" : `: ${errorMessage(probe.error)}`;
|
|
94
|
+
throw new Error(`Could not verify whether workspace for ${ticket} is already live${detail}. Retry or inspect the workspace backend manually before resuming.`);
|
|
95
|
+
}
|
|
96
|
+
if (probe.names.has(ticket)) {
|
|
97
|
+
throw new Error(`Workspace for ${ticket} is already live; attach to it instead of resuming.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function resumeWorkspace(config, options) {
|
|
101
|
+
const ticket = options.ticket.toLowerCase();
|
|
102
|
+
await failIfWorkspaceAlreadyLive(config, ticket);
|
|
103
|
+
const context = await buildResumeContext(config, ticket);
|
|
104
|
+
const definition = config.models.definitions[context.model];
|
|
105
|
+
if (definition === undefined) {
|
|
106
|
+
throw new Error(`Unknown model: ${context.model}`);
|
|
107
|
+
}
|
|
108
|
+
const { runner, sandboxName } = await prepareAgentLaunch({
|
|
109
|
+
config,
|
|
110
|
+
model: context.model,
|
|
111
|
+
definition,
|
|
112
|
+
purpose: "resumes",
|
|
113
|
+
});
|
|
114
|
+
const stagedPrompt = stagePromptText({
|
|
115
|
+
prefix: "groundcrew-resume",
|
|
116
|
+
ticket,
|
|
117
|
+
text: renderResumePrompt(context),
|
|
118
|
+
});
|
|
119
|
+
const secretsFile = stageBuildSecrets(stagedPrompt.directory);
|
|
120
|
+
await ensureAgentSandbox({ config, definition, sandboxName });
|
|
121
|
+
const launchCommand = buildLaunchCommand({
|
|
122
|
+
definition,
|
|
123
|
+
promptFile: stagedPrompt.file,
|
|
124
|
+
worktreeDir: context.worktree.dir,
|
|
125
|
+
secretsFile,
|
|
126
|
+
runner,
|
|
127
|
+
sandboxName,
|
|
128
|
+
});
|
|
129
|
+
const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
|
|
130
|
+
try {
|
|
131
|
+
await openAgentWorkspace({
|
|
132
|
+
config,
|
|
133
|
+
name: ticket,
|
|
134
|
+
cwd: context.worktree.dir,
|
|
135
|
+
command: launchCmd,
|
|
136
|
+
model: context.model,
|
|
137
|
+
color: definition.color,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
removeStagedPrompt(stagedPrompt.directory);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
recordRunState({
|
|
145
|
+
config,
|
|
146
|
+
state: {
|
|
147
|
+
ticket,
|
|
148
|
+
repository: context.repository,
|
|
149
|
+
model: context.model,
|
|
150
|
+
worktreeDir: context.worktree.dir,
|
|
151
|
+
branchName: context.worktree.branchName,
|
|
152
|
+
workspaceName: ticket,
|
|
153
|
+
state: "resumed",
|
|
154
|
+
resumeCount: context.resumeCount + 1,
|
|
155
|
+
...(context.reason === undefined ? {} : { reason: context.reason }),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
log(`Resumed ${ticket} in ${context.worktree.dir} (${context.model})`);
|
|
159
|
+
}
|
|
160
|
+
export async function resumeWorkspaceCli(argv) {
|
|
161
|
+
const config = await loadConfig();
|
|
162
|
+
await resumeWorkspace(config, parseArguments(argv));
|
|
163
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAWD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAqBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA6Gf;AAwHD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
4
|
-
import { ensureClearance } from "@clipboard-health/clearance";
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
5
2
|
import { fetchResolvedIssue } from "../lib/boardSource.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { buildLaunchCommand, shellSingleQuote } from "../lib/launchCommand.js";
|
|
3
|
+
import { loadConfig } from "../lib/config.js";
|
|
4
|
+
import { ensureAgentSandbox, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
5
|
+
import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
10
6
|
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
7
|
+
import { recordRunState } from "../lib/runState.js";
|
|
8
|
+
import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
9
|
+
import { errorMessage, getLinearClient, log } from "../lib/util.js";
|
|
13
10
|
import { workspaces } from "../lib/workspaces.js";
|
|
14
11
|
import { isWorktreeAlreadyExistsError, worktrees } from "../lib/worktrees.js";
|
|
15
12
|
async function fetchTicket(ticket) {
|
|
@@ -20,54 +17,18 @@ async function fetchTicket(ticket) {
|
|
|
20
17
|
description: issue.description ?? "",
|
|
21
18
|
};
|
|
22
19
|
}
|
|
23
|
-
function renderPrompt(template, variables) {
|
|
24
|
-
return template
|
|
25
|
-
.replaceAll("{{ticket}}", variables.ticket)
|
|
26
|
-
.replaceAll("{{worktree}}", variables.worktree)
|
|
27
|
-
.replaceAll("{{title}}", variables.title)
|
|
28
|
-
.replaceAll("{{description}}", variables.description);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Stage a `KEY='value'` env file for any populated build-time secret so
|
|
32
|
-
* the launch command can source it. Returns `undefined` when groundcrew
|
|
33
|
-
* has nothing to forward, leaving the launch command unchanged. The temp
|
|
34
|
-
* dir is `rm -rf`'d by the launch command (and rollback path), so cleanup
|
|
35
|
-
* is already handled.
|
|
36
|
-
*/
|
|
37
|
-
function stageBuildSecrets(promptDir) {
|
|
38
|
-
const lines = [];
|
|
39
|
-
for (const name of BUILD_SECRET_NAMES) {
|
|
40
|
-
const value = readEnvironmentVariable(name);
|
|
41
|
-
if (value === undefined || value.length === 0) {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
lines.push(`${name}=${shellSingleQuote(value)}`);
|
|
45
|
-
}
|
|
46
|
-
if (lines.length === 0) {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
const secretsFile = join(promptDir, "secrets.env");
|
|
50
|
-
writeFileSync(secretsFile, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
51
|
-
return secretsFile;
|
|
52
|
-
}
|
|
53
|
-
function stageLaunchScript(promptDir, command) {
|
|
54
|
-
const launcherFile = join(promptDir, "launch.sh");
|
|
55
|
-
writeFileSync(launcherFile, `#!/usr/bin/env bash\n${command}\n`, { mode: 0o700 });
|
|
56
|
-
return launcherFile;
|
|
57
|
-
}
|
|
58
|
-
function stageWorkspaceLaunchCommand(promptDir, command) {
|
|
59
|
-
return `bash ${shellSingleQuote(stageLaunchScript(promptDir, command))}`;
|
|
60
|
-
}
|
|
61
20
|
function stagePrompt(input) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
21
|
+
return stagePromptFromTemplate({
|
|
22
|
+
config: input.config,
|
|
23
|
+
prefix: "groundcrew",
|
|
65
24
|
ticket: input.ticket,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
25
|
+
variables: {
|
|
26
|
+
ticket: input.ticket,
|
|
27
|
+
worktree: input.worktreeName,
|
|
28
|
+
title: input.ticketDetails.title,
|
|
29
|
+
description: input.ticketDetails.description,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
71
32
|
}
|
|
72
33
|
export async function setupWorkspace(config, options, runOptions = {}) {
|
|
73
34
|
const { ticket, repository, model } = options;
|
|
@@ -76,16 +37,13 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
76
37
|
if (!definition) {
|
|
77
38
|
throw new Error(`Unknown model: ${model}`);
|
|
78
39
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw new Error(`Local groundcrew runs with the sdx runner require a sandbox config on model '${model}'. ` +
|
|
87
|
-
"Add `sandbox: { agent: '<sbx-agent-name>' }` to the model in your config.ts.");
|
|
88
|
-
}
|
|
40
|
+
const { runner, sandboxName } = await prepareAgentLaunch({
|
|
41
|
+
config,
|
|
42
|
+
model,
|
|
43
|
+
definition,
|
|
44
|
+
purpose: "runs",
|
|
45
|
+
...(signal === undefined ? {} : { signal }),
|
|
46
|
+
});
|
|
89
47
|
const spec = { repository, ticket };
|
|
90
48
|
let created;
|
|
91
49
|
try {
|
|
@@ -122,16 +80,12 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
122
80
|
const stagedPrompt = stagePrompt({ config, ticket, ticketDetails, worktreeName });
|
|
123
81
|
promptDir = stagedPrompt.directory;
|
|
124
82
|
const secretsFile = stageBuildSecrets(promptDir);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
sandbox: definition.sandbox,
|
|
132
|
-
mountPath: resolve(config.workspace.projectDir),
|
|
133
|
-
}, signal);
|
|
134
|
-
}
|
|
83
|
+
await ensureAgentSandbox({
|
|
84
|
+
config,
|
|
85
|
+
definition,
|
|
86
|
+
sandboxName,
|
|
87
|
+
...(signal === undefined ? {} : { signal }),
|
|
88
|
+
});
|
|
135
89
|
const launchCommand = buildLaunchCommand({
|
|
136
90
|
definition,
|
|
137
91
|
promptFile: stagedPrompt.file,
|
|
@@ -142,12 +96,25 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
142
96
|
});
|
|
143
97
|
const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
|
|
144
98
|
log("Opening workspace...");
|
|
145
|
-
await
|
|
99
|
+
await openAgentWorkspace({
|
|
100
|
+
config,
|
|
146
101
|
name: ticket,
|
|
147
102
|
cwd: launchDir,
|
|
148
103
|
command: launchCmd,
|
|
149
|
-
|
|
150
|
-
|
|
104
|
+
model,
|
|
105
|
+
color: definition.color,
|
|
106
|
+
...(signal === undefined ? {} : { signal }),
|
|
107
|
+
});
|
|
108
|
+
recordRunStateBestEffort({
|
|
109
|
+
config,
|
|
110
|
+
ticket,
|
|
111
|
+
repository,
|
|
112
|
+
model,
|
|
113
|
+
worktreeDir: launchDir,
|
|
114
|
+
branchName,
|
|
115
|
+
workspaceName: ticket,
|
|
116
|
+
state: "running",
|
|
117
|
+
});
|
|
151
118
|
log(`Workspace "${ticket}" launched (${model})`);
|
|
152
119
|
log(` Worktree: ${launchDir}`);
|
|
153
120
|
log(` Branch: ${branchName}`);
|
|
@@ -155,6 +122,17 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
155
122
|
}
|
|
156
123
|
catch (error) {
|
|
157
124
|
await rollbackWorktree({ config, entry: created, promptDir });
|
|
125
|
+
recordRunStateBestEffort({
|
|
126
|
+
config,
|
|
127
|
+
ticket,
|
|
128
|
+
repository,
|
|
129
|
+
model,
|
|
130
|
+
worktreeDir: launchDir,
|
|
131
|
+
branchName,
|
|
132
|
+
workspaceName: ticket,
|
|
133
|
+
state: "failed-to-launch",
|
|
134
|
+
detail: errorMessage(error),
|
|
135
|
+
});
|
|
158
136
|
throw error;
|
|
159
137
|
}
|
|
160
138
|
}
|
|
@@ -188,6 +166,26 @@ async function logWorkspaceAccessHint(arguments_) {
|
|
|
188
166
|
function logAccessHint(accessHint) {
|
|
189
167
|
log(` Attach: ${accessHint.command}`);
|
|
190
168
|
}
|
|
169
|
+
function recordRunStateBestEffort(arguments_) {
|
|
170
|
+
try {
|
|
171
|
+
recordRunState({
|
|
172
|
+
config: arguments_.config,
|
|
173
|
+
state: {
|
|
174
|
+
ticket: arguments_.ticket,
|
|
175
|
+
repository: arguments_.repository,
|
|
176
|
+
model: arguments_.model,
|
|
177
|
+
worktreeDir: arguments_.worktreeDir,
|
|
178
|
+
branchName: arguments_.branchName,
|
|
179
|
+
workspaceName: arguments_.workspaceName,
|
|
180
|
+
state: arguments_.state,
|
|
181
|
+
...(arguments_.detail === undefined ? {} : { detail: arguments_.detail }),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
log(`Run state update failed for ${arguments_.ticket}: ${errorMessage(error)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
191
189
|
async function rollbackWorktree(arguments_) {
|
|
192
190
|
log(`Setup failed; rolling back worktree ${arguments_.entry.repository}-${arguments_.entry.ticket}...`);
|
|
193
191
|
let result;
|
|
@@ -243,5 +241,6 @@ export async function setupWorkspaceCli(ticket, options = {}) {
|
|
|
243
241
|
id: ticket.toLowerCase(),
|
|
244
242
|
uuid: resolved.uuid,
|
|
245
243
|
teamId: resolved.teamId,
|
|
244
|
+
projectSlugId: resolved.projectSlugId,
|
|
246
245
|
});
|
|
247
246
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Blocker, type RawLinearIssue } from "../lib/boardSource.ts";
|
|
2
2
|
import { type ResolvedConfig } from "../lib/config.ts";
|
|
3
|
+
import { type RunState } from "../lib/runState.ts";
|
|
3
4
|
import { type UsageByModel } from "../lib/usage.ts";
|
|
4
5
|
import { type WorkspaceAccessHint, type WorkspaceProbe } from "../lib/workspaces.ts";
|
|
5
6
|
import { type WorktreeDirtiness, type WorktreeEntry } from "../lib/worktrees.ts";
|
|
@@ -12,6 +13,14 @@ export type TicketDoctorVerdict = {
|
|
|
12
13
|
kind: "pr-merged";
|
|
13
14
|
number: number;
|
|
14
15
|
url: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "interrupted";
|
|
18
|
+
reason: string;
|
|
19
|
+
nextStep: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "failed-launch";
|
|
22
|
+
reason: string;
|
|
23
|
+
nextStep: string;
|
|
15
24
|
} | {
|
|
16
25
|
kind: "in-flight";
|
|
17
26
|
reason: string;
|
|
@@ -99,15 +108,18 @@ export interface DecideVerdictInput {
|
|
|
99
108
|
branch: string;
|
|
100
109
|
worktreeDir: string | undefined;
|
|
101
110
|
workspaceName: string | undefined;
|
|
111
|
+
runState: RunState | undefined;
|
|
102
112
|
}
|
|
103
113
|
/**
|
|
104
114
|
* Returns a post-dispatch verdict if the probe bundle matches one of the
|
|
105
115
|
* "ticket has moved past dispatch" cases. Returns `undefined` otherwise,
|
|
106
116
|
* signalling that the caller should fall through to the pre-dispatch path.
|
|
107
117
|
*
|
|
108
|
-
* Precedence: PR
|
|
109
|
-
*
|
|
110
|
-
*
|
|
118
|
+
* Precedence: PR verdicts always win. Failed launches report before ordinary
|
|
119
|
+
* local recovery. Interrupted runs report concrete recoverable git work first
|
|
120
|
+
* when it exists, then fall back to `interrupted`. Ordinary post-dispatch cases
|
|
121
|
+
* report in-flight before recoverable. Inside `recoverable`, dirty worktree
|
|
122
|
+
* beats clean-with-un-pushed-local beats remote-only beats stranded local.
|
|
111
123
|
*/
|
|
112
124
|
export declare function decidePostDispatchVerdict(input: DecideVerdictInput): TicketDoctorVerdict | undefined;
|
|
113
125
|
export interface TicketDoctorDependencies {
|
|
@@ -147,6 +159,7 @@ export interface TicketDoctorDependencies {
|
|
|
147
159
|
repoDir: string;
|
|
148
160
|
branch: string;
|
|
149
161
|
}) => Promise<PullRequestProbe>;
|
|
162
|
+
readRunState: (ticket: string) => RunState | undefined;
|
|
150
163
|
doFetch: boolean;
|
|
151
164
|
}
|
|
152
165
|
export interface TicketDoctorResult {
|
|
@@ -154,6 +167,7 @@ export interface TicketDoctorResult {
|
|
|
154
167
|
title?: string;
|
|
155
168
|
resolution: TicketCheck[];
|
|
156
169
|
eligibility: TicketCheck[];
|
|
170
|
+
runState: TicketCheck[];
|
|
157
171
|
worktree: TicketCheck[];
|
|
158
172
|
workspace: TicketCheck[];
|
|
159
173
|
localBranch: TicketCheck[];
|
|
@@ -163,6 +177,7 @@ export interface TicketDoctorResult {
|
|
|
163
177
|
resolution: string;
|
|
164
178
|
eligibility: string;
|
|
165
179
|
worktree: string;
|
|
180
|
+
runState: string;
|
|
166
181
|
workspace: string;
|
|
167
182
|
localBranch: string;
|
|
168
183
|
remoteBranch: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ticketDoctor.d.ts","sourceRoot":"","sources":["../../src/commands/ticketDoctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ticketDoctor.d.ts","sourceRoot":"","sources":["../../src/commands/ticketDoctor.ts"],"names":[],"mappings":"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"}
|