@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 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,EACL,KAAK,UAAU,EAIhB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,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,CAoQjE"}
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, } from "../lib/boardSource.js";
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 inProgressStateByTeam = new Map();
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
- teamsMissingInProgress = new Set();
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":"AAwBA,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,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;AA4rBD,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,CA6BlF;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7D"}
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;AAa1B,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,CAoBf"}
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;CACzB;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,CAyCzB"}
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"}
@@ -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 { title: issue.title, description, repository, model, runner };
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":"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"}
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.10.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": "16.4.0",
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",