@clipboard-health/groundcrew 1.10.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli.js +1 -1
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +5 -41
- package/dist/commands/remoteSetup.d.ts +1 -0
- package/dist/commands/remoteSetup.d.ts.map +1 -1
- package/dist/commands/remoteSetup.js +114 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +6 -0
- package/dist/lib/boardSource.d.ts +2 -0
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +11 -1
- package/dist/lib/linearIssueStatus.d.ts +17 -0
- package/dist/lib/linearIssueStatus.d.ts.map +1 -0
- package/dist/lib/linearIssueStatus.js +41 -0
- package/dist/lib/spriteRemoteRunnerProvider.d.ts +3 -0
- package/dist/lib/spriteRemoteRunnerProvider.d.ts.map +1 -1
- package/dist/lib/spriteRemoteRunnerProvider.js +99 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -81,6 +81,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
81
81
|
--claude \
|
|
82
82
|
--codex \
|
|
83
83
|
--copy-local-codex-auth \
|
|
84
|
+
--datadog \
|
|
84
85
|
--github \
|
|
85
86
|
--git-name "Your Name" \
|
|
86
87
|
--git-email "you@users.noreply.github.com" \
|
|
@@ -89,7 +90,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
89
90
|
--checkpoint
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
Known MCP aliases are `linear`, `slack`, and `notion`. For another HTTP MCP server, pass `--mcp name=https://example.com/mcp`. The command creates the remote runner if needed, prepares `~/dev`, configures Git, runs selected auth flows, adds selected MCP servers to Claude Code, and then opens Claude so you can run `/mcp` and authenticate only those selected servers. With the Sprite provider, `--copy-local-codex-auth` copies `${CODEX_HOME:-$HOME/.codex}/auth.json` into `/home/sprite/.codex/auth.json` and then verifies `codex login status`; it never prints the file contents. Use `--skip-mcp-auth` when you only want to add MCP definitions, and run the `/mcp` step later.
|
|
93
|
+
Known MCP aliases are `linear`, `slack`, and `notion`. For another HTTP MCP server, pass `--mcp name=https://example.com/mcp`. The command creates the remote runner if needed, prepares `~/dev`, configures Git, runs selected auth flows, adds selected MCP servers to Claude Code, and then opens Claude so you can run `/mcp` and authenticate only those selected servers. With the Sprite provider, `--copy-local-codex-auth` copies `${CODEX_HOME:-$HOME/.codex}/auth.json` into `/home/sprite/.codex/auth.json` and then verifies `codex login status`; it never prints the file contents. `--datadog` installs a pinned `pup` release in the remote runner, verifies its checksum, installs the `dd-pup` guidance for Claude and Codex, verifies `pup auth status`, and when auth is missing temporarily runs `sprite proxy` for the OAuth callback while `pup auth login --read-only` runs in the remote runner. Open the Datadog URL printed by `pup` if your terminal does not open it automatically. Use `--skip-mcp-auth` when you only want to add MCP definitions, and run the `/mcp` step later.
|
|
93
94
|
|
|
94
95
|
Repo setup is separate from runner setup and should run after the ticket branch exists, immediately before launching an agent. It clones/fetches the repo in the remote runner, checks out the requested branch (creating it from the base branch when it does not exist on origin), forwards only build-time secrets for the dependency install, removes the temporary secret file, clears those env vars, and then exits. It uses groundcrew's remote setup command.
|
|
95
96
|
|
|
@@ -180,7 +181,7 @@ Rules:
|
|
|
180
181
|
## Manual commands
|
|
181
182
|
|
|
182
183
|
```bash
|
|
183
|
-
crew remote setup crew-claude-1 --claude --codex --copy-local-codex-auth --github --mcp linear --checkpoint
|
|
184
|
+
crew remote setup crew-claude-1 --claude --codex --copy-local-codex-auth --datadog --github --mcp linear --checkpoint
|
|
184
185
|
crew remote bootstrap crew-claude-1 core-utils --branch rocky-team-123
|
|
185
186
|
crew remote sessions
|
|
186
187
|
crew remote attach <session-id-or-command> --runner crew-claude-1
|
package/dist/cli.js
CHANGED
|
@@ -62,7 +62,7 @@ const SUBCOMMANDS = {
|
|
|
62
62
|
},
|
|
63
63
|
remote: {
|
|
64
64
|
summary: "Create, authenticate, bootstrap, and inspect a remote runner",
|
|
65
|
-
usage: "setup <runner-name> [--claude] [--github] [--mcp <alias|name=url>] [--checkpoint]\n" +
|
|
65
|
+
usage: "setup <runner-name> [--claude] [--codex] [--datadog] [--github] [--mcp <alias|name=url>] [--checkpoint]\n" +
|
|
66
66
|
" → crew remote bootstrap <runner-name> <repo> [--branch <branch>]\n" +
|
|
67
67
|
" → crew remote sessions [<runner-name>]\n" +
|
|
68
68
|
" → crew remote attach <session-id-or-command> [--runner <runner-name>]\n" +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAczD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAwNjE"}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* Pure verdict logic lives in `eligibility.ts`; this module is responsible
|
|
7
7
|
* for telemetry, Linear writes, and side-effecting setupWorkspace calls.
|
|
8
8
|
*/
|
|
9
|
-
import { isGroundcrewIssue
|
|
9
|
+
import { isGroundcrewIssue } from "../lib/boardSource.js";
|
|
10
|
+
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
10
11
|
import { errorMessage, log, logEvent } from "../lib/util.js";
|
|
11
12
|
import { workspaces } from "../lib/workspaces.js";
|
|
12
13
|
import { classifyBlockers, classifyEligibility, } from "./eligibility.js";
|
|
@@ -17,8 +18,7 @@ const MINUTES_PER_DAY = 24 * 60;
|
|
|
17
18
|
const MINUTES_PER_WEEK = DAYS_PER_WEEK * MINUTES_PER_DAY;
|
|
18
19
|
export function createDispatcher(deps) {
|
|
19
20
|
const { config, client } = deps;
|
|
20
|
-
const
|
|
21
|
-
let teamsMissingInProgress = new Set();
|
|
21
|
+
const issueStatusUpdater = createLinearIssueStatusUpdater({ config, client });
|
|
22
22
|
function buildExhaustedSet(usage) {
|
|
23
23
|
const exhausted = new Set();
|
|
24
24
|
const sessionLimit = config.orchestrator.sessionLimitPercentage;
|
|
@@ -49,42 +49,6 @@ export function createDispatcher(deps) {
|
|
|
49
49
|
}
|
|
50
50
|
return exhausted;
|
|
51
51
|
}
|
|
52
|
-
async function getInProgressStateId(teamId) {
|
|
53
|
-
if (teamId.length === 0) {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
const cached = inProgressStateByTeam.get(teamId);
|
|
57
|
-
if (cached !== undefined) {
|
|
58
|
-
return cached;
|
|
59
|
-
}
|
|
60
|
-
// Negative cache is per-iteration so a team that's fixed in Linear during
|
|
61
|
-
// a `crew watch` session auto-recovers next tick. Within one iteration,
|
|
62
|
-
// every eligible ticket in a misconfigured team would otherwise re-fetch.
|
|
63
|
-
if (teamsMissingInProgress.has(teamId)) {
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
const team = await client.team(teamId);
|
|
67
|
-
const states = await team.states();
|
|
68
|
-
const inProgress = states.nodes.find((state) => state.name === config.linear.statuses.inProgress);
|
|
69
|
-
if (inProgress?.id === undefined) {
|
|
70
|
-
teamsMissingInProgress.add(teamId);
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
inProgressStateByTeam.set(teamId, inProgress.id);
|
|
74
|
-
return inProgress.id;
|
|
75
|
-
}
|
|
76
|
-
async function markInProgress(issue) {
|
|
77
|
-
const stateId = await getInProgressStateId(issue.teamId);
|
|
78
|
-
if (stateId === undefined) {
|
|
79
|
-
// Throw rather than log+return: if we silently swallowed this, the
|
|
80
|
-
// ticket would stay Todo forever while the workspace runs, which means
|
|
81
|
-
// every iteration re-enters the recovery path and the agent never
|
|
82
|
-
// counts toward maximumInProgress.
|
|
83
|
-
throw new Error(`Could not find "${config.linear.statuses.inProgress}" state for ${issue.id} (team ${issue.teamId.length > 0 ? issue.teamId : "?"}). Verify the status name in linear.statuses.inProgress matches the team's workflow.`);
|
|
84
|
-
}
|
|
85
|
-
await client.updateIssue(issue.uuid, { stateId });
|
|
86
|
-
log(`Marked ${issue.id} as ${config.linear.statuses.inProgress}`);
|
|
87
|
-
}
|
|
88
52
|
function logSkip(verdict) {
|
|
89
53
|
log(verdict.message);
|
|
90
54
|
logEvent("dispatch", {
|
|
@@ -130,7 +94,7 @@ export function createDispatcher(deps) {
|
|
|
130
94
|
? setupWorkspace(config, setupOptions)
|
|
131
95
|
: setupWorkspace(config, setupOptions, { signal }));
|
|
132
96
|
}
|
|
133
|
-
await markInProgress(issue);
|
|
97
|
+
await issueStatusUpdater.markInProgress(issue);
|
|
134
98
|
logEvent("dispatch", {
|
|
135
99
|
outcome: recovery ? "resumed" : "started",
|
|
136
100
|
ticket: issue.id,
|
|
@@ -153,7 +117,7 @@ export function createDispatcher(deps) {
|
|
|
153
117
|
}
|
|
154
118
|
async function runOnce(arguments_) {
|
|
155
119
|
const { state, worktreeEntries, usage, dryRun, signal } = arguments_;
|
|
156
|
-
|
|
120
|
+
issueStatusUpdater.resetMissingInProgressCache();
|
|
157
121
|
const activeCount = state.issues.filter((issue) => issue.status === config.linear.statuses.inProgress).length;
|
|
158
122
|
const slots = config.orchestrator.maximumInProgress - activeCount;
|
|
159
123
|
// Narrow Todo to tickets that opted in via an `agent-*` label.
|
|
@@ -8,6 +8,7 @@ export interface RemoteSetupOptions {
|
|
|
8
8
|
shouldAuthenticateClaude: boolean;
|
|
9
9
|
shouldAuthenticateCodex: boolean;
|
|
10
10
|
shouldCopyLocalCodexAuth?: boolean;
|
|
11
|
+
shouldSetupDatadog?: boolean;
|
|
11
12
|
shouldAuthenticateGithub: boolean;
|
|
12
13
|
shouldAuthenticateMcp: boolean;
|
|
13
14
|
shouldCheckpoint: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remoteSetup.d.ts","sourceRoot":"","sources":["../../src/commands/remoteSetup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"remoteSetup.d.ts","sourceRoot":"","sources":["../../src/commands/remoteSetup.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,wBAAwB,EAAE,OAAO,CAAC;IAClC,uBAAuB,EAAE,OAAO,CAAC;IACjC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,wBAAwB,EAAE,OAAO,CAAC;IAClC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,4BAA4B,EAAE,OAAO,CAAC;IACtC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAy0BD,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B9F;AAUD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAItF;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrF;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAItF;AAED,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhG;AAYD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgClF;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7D"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
5
|
import { BUILD_SECRET_NAMES, DEFAULT_REMOTE_SETUP_COMMAND, loadConfig, } from "../lib/config.js";
|
|
5
6
|
import { shellSingleQuote } from "../lib/shell.js";
|
|
6
7
|
import { getRemoteRunnerProvider, remoteConfigWithRunnerName, } from "../lib/spriteRemoteRunnerProvider.js";
|
|
@@ -14,6 +15,10 @@ const DEFAULT_CHECKPOINT_COMMENT = "groundcrew remote runner baseline: selected
|
|
|
14
15
|
const CLAUDE_SUBSCRIPTION_LOGIN_FLAG = ["--claude", "ai"].join("");
|
|
15
16
|
const DEFAULT_REPOSITORY_OWNER = "ClipboardHealth";
|
|
16
17
|
const DEFAULT_BASE_BRANCH = "main";
|
|
18
|
+
const DATADOG_PUP_VERSION = "0.63.0";
|
|
19
|
+
const DATADOG_OAUTH_CALLBACK_PORT = 8000;
|
|
20
|
+
const DATADOG_AUTH_STATUS_RETRY_ATTEMPTS = 5;
|
|
21
|
+
const DATADOG_AUTH_STATUS_RETRY_DELAY_MS = 250;
|
|
17
22
|
const REMOTE_SECRETS_FILE = "/tmp/groundcrew-build-secrets.env";
|
|
18
23
|
const REMOTE_CODEX_AUTH_UPLOAD_FILE = "/tmp/groundcrew-codex-auth.json";
|
|
19
24
|
const REMOTE_CODEX_AUTH_FILE = "/home/sprite/.codex/auth.json";
|
|
@@ -31,6 +36,7 @@ function usage() {
|
|
|
31
36
|
" --claude Authenticate Claude Code with a Claude subscription",
|
|
32
37
|
" --codex Authenticate Codex CLI",
|
|
33
38
|
" --copy-local-codex-auth With --codex, copy local CODEX_HOME auth.json into the remote runner",
|
|
39
|
+
" --datadog Install pup, add dd-pup skills, and authenticate Datadog",
|
|
34
40
|
" --github Authenticate gh for GitHub PRs",
|
|
35
41
|
" --mcp <alias|name=url> Add/authenticate one MCP server; repeat for multiple",
|
|
36
42
|
" Known aliases: linear, slack, notion",
|
|
@@ -134,6 +140,7 @@ function parseArguments(argv) {
|
|
|
134
140
|
let shouldAuthenticateClaude = false;
|
|
135
141
|
let shouldAuthenticateCodex = false;
|
|
136
142
|
let shouldCopyLocalCodexAuth = false;
|
|
143
|
+
let shouldSetupDatadog = false;
|
|
137
144
|
let shouldAuthenticateGithub = false;
|
|
138
145
|
let shouldAuthenticateMcp = true;
|
|
139
146
|
let shouldCheckpoint = false;
|
|
@@ -156,6 +163,10 @@ function parseArguments(argv) {
|
|
|
156
163
|
shouldCopyLocalCodexAuth = true;
|
|
157
164
|
continue;
|
|
158
165
|
}
|
|
166
|
+
if (argument === "--datadog") {
|
|
167
|
+
shouldSetupDatadog = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
159
170
|
if (argument === "--github") {
|
|
160
171
|
shouldAuthenticateGithub = true;
|
|
161
172
|
continue;
|
|
@@ -202,6 +213,7 @@ function parseArguments(argv) {
|
|
|
202
213
|
shouldAuthenticateClaude,
|
|
203
214
|
shouldAuthenticateCodex,
|
|
204
215
|
shouldCopyLocalCodexAuth,
|
|
216
|
+
shouldSetupDatadog,
|
|
205
217
|
shouldAuthenticateGithub,
|
|
206
218
|
shouldAuthenticateMcp,
|
|
207
219
|
shouldCheckpoint,
|
|
@@ -457,6 +469,105 @@ async function authenticateCodex(arguments_) {
|
|
|
457
469
|
}
|
|
458
470
|
throw new Error("Codex login finished, but `codex login status` still reports not logged in inside the remote runner. Try `crew remote setup <runner-name> --codex --copy-local-codex-auth`.");
|
|
459
471
|
}
|
|
472
|
+
function datadogPupInstallCommand() {
|
|
473
|
+
const shellVariable = "$";
|
|
474
|
+
return [
|
|
475
|
+
"set -euo pipefail",
|
|
476
|
+
`version=${shellSingleQuote(DATADOG_PUP_VERSION)}`,
|
|
477
|
+
'case "$(uname -m)" in',
|
|
478
|
+
" x86_64|amd64) arch=x86_64 ;;",
|
|
479
|
+
" aarch64|arm64) arch=arm64 ;;",
|
|
480
|
+
' *) echo "Unsupported Linux architecture: $(uname -m)" >&2; exit 1 ;;',
|
|
481
|
+
"esac",
|
|
482
|
+
`asset="pup_${shellVariable}{version}_Linux_${shellVariable}{arch}.tar.gz"`,
|
|
483
|
+
`base_url="https://github.com/DataDog/pup/releases/download/v${shellVariable}{version}"`,
|
|
484
|
+
'binary="$HOME/.local/bin/pup"',
|
|
485
|
+
'mkdir -p "$HOME/.local/bin"',
|
|
486
|
+
`if [ -x "$binary" ] && "$binary" version | grep -q "Pup ${shellVariable}{version} "; then "$binary" version; exit 0; fi`,
|
|
487
|
+
'install_dir="$(mktemp -d)"',
|
|
488
|
+
"trap 'rm -rf \"$install_dir\"' EXIT",
|
|
489
|
+
`archive="${shellVariable}{install_dir}/${shellVariable}{asset}"`,
|
|
490
|
+
`checksums="${shellVariable}{install_dir}/pup_${shellVariable}{version}_checksums.txt"`,
|
|
491
|
+
'curl -fsSL -o "$archive" "$base_url/$asset"',
|
|
492
|
+
`curl -fsSL -o "$checksums" "$base_url/pup_${shellVariable}{version}_checksums.txt"`,
|
|
493
|
+
`(cd "$install_dir" && grep " ${shellVariable}{asset}$" "$checksums" | sha256sum -c -)`,
|
|
494
|
+
'tar -xzf "$archive" -C "$HOME/.local/bin" pup',
|
|
495
|
+
'chmod 755 "$binary"',
|
|
496
|
+
'"$binary" version',
|
|
497
|
+
].join("\n");
|
|
498
|
+
}
|
|
499
|
+
async function installDatadogPup(provider, config) {
|
|
500
|
+
log(`Installing Datadog pup ${DATADOG_PUP_VERSION} inside the remote runner`);
|
|
501
|
+
await provider.runCommand({
|
|
502
|
+
config,
|
|
503
|
+
remoteArguments: ["bash", "-lc", datadogPupInstallCommand()],
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async function installDatadogPupSkills(provider, config) {
|
|
507
|
+
for (const platform of ["claude-code", "codex"]) {
|
|
508
|
+
// oxlint-disable-next-line no-await-in-loop -- installs are quick and ordered so logs stay readable.
|
|
509
|
+
await provider.runCommand({
|
|
510
|
+
config,
|
|
511
|
+
remoteArguments: ["pup", "skills", "install", platform, "--name", "dd-pup", "--yes"],
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function validateDatadogLogin(provider, config) {
|
|
516
|
+
return await commandSucceeds({
|
|
517
|
+
provider,
|
|
518
|
+
config,
|
|
519
|
+
remoteArguments: ["pup", "auth", "status"],
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
async function waitForDatadogLogin(provider, config) {
|
|
523
|
+
for (let attempt = 1; attempt <= DATADOG_AUTH_STATUS_RETRY_ATTEMPTS; attempt += 1) {
|
|
524
|
+
// oxlint-disable-next-line no-await-in-loop -- bounded polling smooths over Datadog OAuth propagation delay.
|
|
525
|
+
if (await validateDatadogLogin(provider, config)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
if (attempt < DATADOG_AUTH_STATUS_RETRY_ATTEMPTS) {
|
|
529
|
+
// oxlint-disable-next-line no-await-in-loop -- retries are intentionally spaced.
|
|
530
|
+
await sleep(DATADOG_AUTH_STATUS_RETRY_DELAY_MS);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
async function authenticateDatadog(provider, config) {
|
|
536
|
+
if (await validateDatadogLogin(provider, config)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
writeOutput();
|
|
540
|
+
writeOutput("Datadog OAuth will run inside the remote runner.");
|
|
541
|
+
writeOutput(`Groundcrew is forwarding local port ${DATADOG_OAUTH_CALLBACK_PORT} to the remote callback server while login runs.`);
|
|
542
|
+
writeOutput("Open the Datadog URL printed by pup if your terminal does not open it for you.");
|
|
543
|
+
writeOutput();
|
|
544
|
+
const proxy = await provider.startPortProxy(config, DATADOG_OAUTH_CALLBACK_PORT);
|
|
545
|
+
try {
|
|
546
|
+
await provider.runTtyCommand({
|
|
547
|
+
config,
|
|
548
|
+
remoteArguments: [
|
|
549
|
+
"pup",
|
|
550
|
+
"auth",
|
|
551
|
+
"login",
|
|
552
|
+
"--read-only",
|
|
553
|
+
"--callback-port",
|
|
554
|
+
String(DATADOG_OAUTH_CALLBACK_PORT),
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
finally {
|
|
559
|
+
await proxy.close();
|
|
560
|
+
}
|
|
561
|
+
if (await waitForDatadogLogin(provider, config)) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
throw new Error("Datadog auth finished, but `pup auth status` still reports not logged in inside the remote runner.");
|
|
565
|
+
}
|
|
566
|
+
async function setupDatadog(provider, config) {
|
|
567
|
+
await installDatadogPup(provider, config);
|
|
568
|
+
await installDatadogPupSkills(provider, config);
|
|
569
|
+
await authenticateDatadog(provider, config);
|
|
570
|
+
}
|
|
460
571
|
async function addMcpServer(arguments_) {
|
|
461
572
|
const { provider, config, server } = arguments_;
|
|
462
573
|
const exists = await commandSucceeds({
|
|
@@ -661,6 +772,9 @@ export async function setupRemoteRunner(options) {
|
|
|
661
772
|
shouldCopyLocalAuth: options.shouldCopyLocalCodexAuth === true,
|
|
662
773
|
});
|
|
663
774
|
}
|
|
775
|
+
if (options.shouldSetupDatadog === true) {
|
|
776
|
+
await setupDatadog(provider, config);
|
|
777
|
+
}
|
|
664
778
|
for (const server of options.mcpServers) {
|
|
665
779
|
// oxlint-disable-next-line no-await-in-loop -- MCP additions are sequential so auth instructions stay ordered.
|
|
666
780
|
await addMcpServer({ provider, config, server });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,kBAAkB,CAAC;AAc1B,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAsED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Ef;AAuHD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
|
@@ -6,6 +6,7 @@ import { fetchResolvedIssue } from "../lib/boardSource.js";
|
|
|
6
6
|
import { BUILD_SECRET_NAMES, loadConfig, } from "../lib/config.js";
|
|
7
7
|
import { detectHostCapabilities } from "../lib/host.js";
|
|
8
8
|
import { buildLaunchCommand, buildRemoteLaunchCommand, shellSingleQuote, } from "../lib/launchCommand.js";
|
|
9
|
+
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
9
10
|
import { assertLocalRunnerRequirements } from "../lib/localRunner.js";
|
|
10
11
|
import { getRemoteRunnerProvider } from "../lib/spriteRemoteRunnerProvider.js";
|
|
11
12
|
import { errorMessage, getLinearClient, log, readEnvironmentVariable } from "../lib/util.js";
|
|
@@ -245,4 +246,9 @@ export async function setupWorkspaceCli(ticket, options = {}) {
|
|
|
245
246
|
runner: resolved.runner,
|
|
246
247
|
details: { title: resolved.title, description: resolved.description },
|
|
247
248
|
});
|
|
249
|
+
await createLinearIssueStatusUpdater({ config, client }).markInProgress({
|
|
250
|
+
id: ticket.toLowerCase(),
|
|
251
|
+
uuid: resolved.uuid,
|
|
252
|
+
teamId: resolved.teamId,
|
|
253
|
+
});
|
|
248
254
|
}
|
|
@@ -65,11 +65,13 @@ interface BoardSourceDeps {
|
|
|
65
65
|
export declare function createBoardSource(deps: BoardSourceDeps): BoardSource;
|
|
66
66
|
export declare function isTerminalStatus(status: string, config: ResolvedConfig): boolean;
|
|
67
67
|
interface ResolvedIssue {
|
|
68
|
+
uuid: string;
|
|
68
69
|
title: string;
|
|
69
70
|
description: string;
|
|
70
71
|
repository: string;
|
|
71
72
|
model: string;
|
|
72
73
|
runner: WorkspaceRunner;
|
|
74
|
+
teamId: string;
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
75
77
|
* `agent-any` collapses to `models.default` here — manual setup doesn't run
|
|
@@ -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,EAGL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAMrB,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,0FAA0F;IAC1F,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC;IACpC,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;IACnB,MAAM,EAAE,eAAe,CAAC;CACzB,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;AAgMD,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,CAAC;
|
|
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,EAGL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAMrB,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,0FAA0F;IAC1F,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC;IACpC,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;IACnB,MAAM,EAAE,eAAe,CAAC;CACzB,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;AAgMD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,EAAE;IACnD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,aAAa,CAAC,CAqDzB"}
|
package/dist/lib/boardSource.js
CHANGED
|
@@ -180,8 +180,10 @@ export async function fetchResolvedIssue(arguments_) {
|
|
|
180
180
|
const { client, config, ticket } = arguments_;
|
|
181
181
|
const response = await client.client.rawRequest(`query ResolveIssue($id: String!) {
|
|
182
182
|
issue(id: $id) {
|
|
183
|
+
id
|
|
183
184
|
title
|
|
184
185
|
description
|
|
186
|
+
team { id }
|
|
185
187
|
labels(first: ${ISSUE_LABEL_PAGE_SIZE}) {
|
|
186
188
|
nodes { name }
|
|
187
189
|
}
|
|
@@ -206,7 +208,15 @@ export async function fetchResolvedIssue(arguments_) {
|
|
|
206
208
|
warnIfDisabledFallback(ticket, parsed, config);
|
|
207
209
|
const model = parsed === undefined || parsed.model === AGENT_ANY_MODEL ? config.models.default : parsed.model;
|
|
208
210
|
const runner = parsed?.runner ?? "local";
|
|
209
|
-
return {
|
|
211
|
+
return {
|
|
212
|
+
uuid: issue.id,
|
|
213
|
+
title: issue.title,
|
|
214
|
+
description,
|
|
215
|
+
repository,
|
|
216
|
+
model,
|
|
217
|
+
runner,
|
|
218
|
+
teamId: issue.team?.id ?? "",
|
|
219
|
+
};
|
|
210
220
|
}
|
|
211
221
|
function parseRepository(arguments_) {
|
|
212
222
|
const { description, config, repositoryRegex, ticket } = arguments_;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LinearClient } from "@linear/sdk";
|
|
2
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
3
|
+
interface LinearIssueReference {
|
|
4
|
+
id: string;
|
|
5
|
+
uuid: string;
|
|
6
|
+
teamId: string;
|
|
7
|
+
}
|
|
8
|
+
interface LinearIssueStatusUpdater {
|
|
9
|
+
markInProgress(issue: LinearIssueReference): Promise<void>;
|
|
10
|
+
resetMissingInProgressCache(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function createLinearIssueStatusUpdater(arguments_: {
|
|
13
|
+
config: ResolvedConfig;
|
|
14
|
+
client: LinearClient;
|
|
15
|
+
}): LinearIssueStatusUpdater;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=linearIssueStatus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linearIssueStatus.d.ts","sourceRoot":"","sources":["../../src/lib/linearIssueStatus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,wBAAwB;IAChC,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,2BAA2B,IAAI,IAAI,CAAC;CACrC;AAED,wBAAgB,8BAA8B,CAAC,UAAU,EAAE;IACzD,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,wBAAwB,CAgD3B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { log } from "./util.js";
|
|
2
|
+
export function createLinearIssueStatusUpdater(arguments_) {
|
|
3
|
+
const { config, client } = arguments_;
|
|
4
|
+
const inProgressStateByTeam = new Map();
|
|
5
|
+
let teamsMissingInProgress = new Set();
|
|
6
|
+
async function getInProgressStateId(teamId) {
|
|
7
|
+
if (teamId.length === 0) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const cached = inProgressStateByTeam.get(teamId);
|
|
11
|
+
if (cached !== undefined) {
|
|
12
|
+
return cached;
|
|
13
|
+
}
|
|
14
|
+
// Negative cache is reset by dispatcher each iteration so a team that's
|
|
15
|
+
// fixed in Linear during a watch session auto-recovers on the next tick.
|
|
16
|
+
if (teamsMissingInProgress.has(teamId)) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const team = await client.team(teamId);
|
|
20
|
+
const states = await team.states();
|
|
21
|
+
const inProgress = states.nodes.find((state) => state.name === config.linear.statuses.inProgress);
|
|
22
|
+
if (inProgress?.id === undefined) {
|
|
23
|
+
teamsMissingInProgress.add(teamId);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
inProgressStateByTeam.set(teamId, inProgress.id);
|
|
27
|
+
return inProgress.id;
|
|
28
|
+
}
|
|
29
|
+
async function markInProgress(issue) {
|
|
30
|
+
const stateId = await getInProgressStateId(issue.teamId);
|
|
31
|
+
if (stateId === undefined) {
|
|
32
|
+
throw new Error(`Could not find "${config.linear.statuses.inProgress}" state for ${issue.id} (team ${issue.teamId.length > 0 ? issue.teamId : "?"}). Verify the status name in linear.statuses.inProgress matches the team's workflow.`);
|
|
33
|
+
}
|
|
34
|
+
await client.updateIssue(issue.uuid, { stateId });
|
|
35
|
+
log(`Marked ${issue.id} as ${config.linear.statuses.inProgress}`);
|
|
36
|
+
}
|
|
37
|
+
function resetMissingInProgressCache() {
|
|
38
|
+
teamsMissingInProgress = new Set();
|
|
39
|
+
}
|
|
40
|
+
return { markInProgress, resetMissingInProgressCache };
|
|
41
|
+
}
|
|
@@ -54,6 +54,9 @@ export interface RemoteRunnerProvider {
|
|
|
54
54
|
runCommand(arguments_: RemoteRunArguments): Promise<string | undefined>;
|
|
55
55
|
runTtyCommand(arguments_: RemoteRunArguments): Promise<void>;
|
|
56
56
|
buildTtyCommand(arguments_: RemoteTtyCommandArguments): string;
|
|
57
|
+
startPortProxy(config: RemoteRunnerConfig, port: number): Promise<{
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
}>;
|
|
57
60
|
listSessions(config: RemoteRunnerConfig): Promise<string>;
|
|
58
61
|
attachSession(config: RemoteRunnerConfig, target: string): Promise<void>;
|
|
59
62
|
listProcesses(config: RemoteRunnerConfig): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spriteRemoteRunnerProvider.d.ts","sourceRoot":"","sources":["../../src/lib/spriteRemoteRunnerProvider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"spriteRemoteRunnerProvider.d.ts","sourceRoot":"","sources":["../../src/lib/spriteRemoteRunnerProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
1
3
|
import { runCommandAsync } from "./commandRunner.js";
|
|
2
4
|
import { shellSingleQuote } from "./shell.js";
|
|
3
5
|
const LONG_RUNNING_COMMAND_OPTIONS = { stdio: "inherit", timeoutMs: 0 };
|
|
6
|
+
const PROXY_CLOSE_SIGNAL = "SIGINT";
|
|
7
|
+
const PROXY_READY_HOST = "127.0.0.1";
|
|
8
|
+
const PROXY_READY_TIMEOUT_MS = 5000;
|
|
9
|
+
const PROXY_READY_RETRY_DELAY_MS = 25;
|
|
4
10
|
export const SPRITE_REMOTE_PROVIDER_DEFAULTS = {
|
|
5
11
|
provider: "sprite",
|
|
6
12
|
runnerName: "crew-claude-1",
|
|
@@ -155,6 +161,96 @@ async function createSpriteRunner(config) {
|
|
|
155
161
|
timeoutMs: 0,
|
|
156
162
|
});
|
|
157
163
|
}
|
|
164
|
+
async function startSpritePortProxy(config, port) {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
let closeWasRequested = false;
|
|
167
|
+
let proxyError;
|
|
168
|
+
const proxy = (async () => {
|
|
169
|
+
try {
|
|
170
|
+
await runCommandAsync("sprite", ["proxy", "-s", config.runnerName, String(port)], {
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
stdio: "inherit",
|
|
173
|
+
timeoutMs: 0,
|
|
174
|
+
});
|
|
175
|
+
if (!closeWasRequested) {
|
|
176
|
+
proxyError = new Error("Sprite proxy exited before it was closed.");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (closeWasRequested && errorHasSignal(error, PROXY_CLOSE_SIGNAL)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
proxyError = new Error(`Sprite proxy exited before it was closed: ${String(error)}`, {
|
|
184
|
+
cause: error,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
})();
|
|
188
|
+
try {
|
|
189
|
+
await waitForSpritePortProxy({ port, proxy, proxyError: () => proxyError });
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
closeWasRequested = true;
|
|
193
|
+
controller.abort();
|
|
194
|
+
await proxy;
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
async close() {
|
|
199
|
+
closeWasRequested = true;
|
|
200
|
+
controller.abort();
|
|
201
|
+
await proxy;
|
|
202
|
+
if (proxyError !== undefined) {
|
|
203
|
+
throw new Error(proxyError.message, { cause: proxyError });
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function waitForSpritePortProxy(arguments_) {
|
|
209
|
+
const deadline = Date.now() + PROXY_READY_TIMEOUT_MS;
|
|
210
|
+
while (Date.now() < deadline) {
|
|
211
|
+
throwIfProxyExited(arguments_.proxyError());
|
|
212
|
+
// eslint-disable-next-line no-await-in-loop -- readiness polling must observe attempts sequentially.
|
|
213
|
+
if (await canConnectToLocalPort(arguments_.port)) {
|
|
214
|
+
throwIfProxyExited(arguments_.proxyError());
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
throwIfProxyExited(arguments_.proxyError());
|
|
218
|
+
// eslint-disable-next-line no-await-in-loop -- retry delay is bounded and stops early if the proxy exits.
|
|
219
|
+
await Promise.race([sleep(PROXY_READY_RETRY_DELAY_MS), arguments_.proxy]);
|
|
220
|
+
}
|
|
221
|
+
throwIfProxyExited(arguments_.proxyError());
|
|
222
|
+
throw new Error(`Timed out waiting for Sprite proxy on ${PROXY_READY_HOST}:${arguments_.port} to accept connections.`);
|
|
223
|
+
}
|
|
224
|
+
function throwIfProxyExited(error) {
|
|
225
|
+
if (error !== undefined) {
|
|
226
|
+
throw new Error(error.message, { cause: error });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function canConnectToLocalPort(port) {
|
|
230
|
+
return await new Promise((resolve) => {
|
|
231
|
+
const socket = connect({ host: PROXY_READY_HOST, port });
|
|
232
|
+
socket.once("connect", () => {
|
|
233
|
+
socket.destroy();
|
|
234
|
+
resolve(true);
|
|
235
|
+
});
|
|
236
|
+
socket.once("error", () => {
|
|
237
|
+
socket.destroy();
|
|
238
|
+
resolve(false);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function errorHasSignal(error, signal) {
|
|
243
|
+
if (typeof error !== "object" || error === null) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if ("signal" in error && error.signal === signal) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
if (error instanceof Error && error.cause !== undefined) {
|
|
250
|
+
return errorHasSignal(error.cause, signal);
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
158
254
|
export const spriteRemoteRunnerProvider = {
|
|
159
255
|
name: "sprite",
|
|
160
256
|
async runnerExists(config) {
|
|
@@ -174,6 +270,9 @@ export const spriteRemoteRunnerProvider = {
|
|
|
174
270
|
});
|
|
175
271
|
},
|
|
176
272
|
buildTtyCommand: buildSpriteTtyCommand,
|
|
273
|
+
async startPortProxy(config, port) {
|
|
274
|
+
return await startSpritePortProxy(config, port);
|
|
275
|
+
},
|
|
177
276
|
async listSessions(config) {
|
|
178
277
|
const output = await runCommandAsync("sprite", ["sessions", "list", "-s", config.runnerName], {
|
|
179
278
|
trim: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle, remote runners, and usage tracking.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"husky": "9.1.7",
|
|
85
85
|
"jscpd": "4.0.9",
|
|
86
86
|
"knip": "6.9.0",
|
|
87
|
-
"lint-staged": "
|
|
87
|
+
"lint-staged": "17.0.4",
|
|
88
88
|
"markdownlint-cli2": "0.22.1",
|
|
89
89
|
"nx": "22.7.1",
|
|
90
90
|
"oxfmt": "0.47.0",
|