@clipboard-health/groundcrew 1.12.0 → 1.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -33
- package/dist/commands/remoteSetup.d.ts +1 -0
- package/dist/commands/remoteSetup.d.ts.map +1 -1
- package/dist/commands/remoteSetup.js +31 -15
- package/dist/lib/spriteRemoteRunnerProvider.d.ts +1 -0
- package/dist/lib/spriteRemoteRunnerProvider.d.ts.map +1 -1
- package/dist/lib/spriteRemoteRunnerProvider.js +17 -6
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +113 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
102
102
|
|
|
103
103
|
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.
|
|
104
104
|
|
|
105
|
-
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
|
|
105
|
+
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 using `git.remote`, checks out the requested branch (creating it from the base branch when it does not exist on that remote), 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.
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
108
|
op run --env-file "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/op.env" -- \
|
|
@@ -126,37 +126,37 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
126
126
|
|
|
127
127
|
Required fields are marked **required**; everything else has a default and can be omitted from `config.ts`.
|
|
128
128
|
|
|
129
|
-
| Key | Default | What it does
|
|
130
|
-
| --------------------------------------- | ------------------------------------- |
|
|
131
|
-
| `linear.projectSlug` | **required** | Linear project URL slug (e.g. `ai-strategy-5152195762f3`). The trailing 12-char hex `slugId` is what's matched against Linear's API; the leading name keeps `config.ts` self-documenting and the lookup survives project renames.
|
|
132
|
-
| `linear.statuses.todo` | `"Todo"` | Status name picked up for new work.
|
|
133
|
-
| `linear.statuses.inProgress` | `"In Progress"` | Status set after a workspace is provisioned; counts toward `maximumInProgress`.
|
|
134
|
-
| `linear.statuses.done` | `"Done"` | Status that triggers worktree cleanup.
|
|
135
|
-
| `linear.statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup, board remaining counts, and blocker checks. The `done` status is always included.
|
|
136
|
-
| `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref.
|
|
137
|
-
| `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base.
|
|
138
|
-
| `workspace.projectDir` | **required** | Parent dir for cloned repos and local sibling ticket worktrees. Remote ticket worktrees live in `remote.worktreeRoot` on the remote runner.
|
|
139
|
-
| `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored.
|
|
140
|
-
| `orchestrator.maximumInProgress` | `4` | Cap on tickets in `linear.statuses.inProgress` at once.
|
|
141
|
-
| `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode.
|
|
142
|
-
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick.
|
|
143
|
-
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew
|
|
144
|
-
| `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults.
|
|
145
|
-
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Local macOS runs execute in the worktree through Safehouse/clearance; remote runs execute inside the remote worktree. `{{worktree}}` is replaced before launch and legacy `{{sandbox}}` expands to an empty string.
|
|
146
|
-
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it).
|
|
147
|
-
| `models.definitions.<name>.usage` | optional | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage`. Omit to never gate. When `usage.codexbar.source` is omitted, groundcrew uses `auto` on macOS and `cli` elsewhere.
|
|
148
|
-
| `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. See "Disabling a shipped default" below.
|
|
149
|
-
| `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`.
|
|
150
|
-
| `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"tmux"` to fail loudly when the chosen backend is missing. tmux windows live in a dedicated `groundcrew` session.
|
|
151
|
-
| `remote.provider` | `"sprite"` | Remote runner provider used for tickets labeled `agent-remote`. Sprite is currently the only provider.
|
|
152
|
-
| `remote.runnerName` | `"crew-claude-1"` | Remote runner used for tickets labeled `agent-remote`.
|
|
153
|
-
| `remote.owner` | `"ClipboardHealth"` | GitHub owner used when cloning bare repository names inside the remote runner.
|
|
154
|
-
| `remote.repoRoot` | `"/home/sprite/dev"` | Shared clone root inside the remote runner.
|
|
155
|
-
| `remote.worktreeRoot` | `"/home/sprite/groundcrew/worktrees"` | Per-ticket remote worktree root inside the remote runner.
|
|
156
|
-
| `remote.secretNames` | `["NPM_TOKEN", "BUF_TOKEN"]` | Build-only env vars that may be uploaded for remote dependency setup, then unset before the agent process starts.
|
|
157
|
-
| `logging.file` | XDG state path | Append-mode log file destination. `log()` / `logEvent()` tee here in addition to stdout, so a vanished workspace doesn't take the evidence with it. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`.
|
|
158
|
-
|
|
159
|
-
The branch prefix (`<prefix>-<TICKET>`) is derived from your OS username (`os.userInfo().username`), not configured. Agent selection looks for a top-level Linear label named `agent-<model>` (e.g. `agent-claude`, `agent-codex`). Add `agent-remote` to run that ticket in the configured remote runner instead of locally; `agent-remote` is a modifier label, not a model. **`crew run` only fetches tickets with an `agent-*` label** — the GraphQL query filters them server-side, so unlabeled tickets are never returned by Linear's API and do not appear in the rendered board. Use `crew
|
|
129
|
+
| Key | Default | What it does |
|
|
130
|
+
| --------------------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `linear.projectSlug` | **required** | Linear project URL slug (e.g. `ai-strategy-5152195762f3`). The trailing 12-char hex `slugId` is what's matched against Linear's API; the leading name keeps `config.ts` self-documenting and the lookup survives project renames. |
|
|
132
|
+
| `linear.statuses.todo` | `"Todo"` | Status name picked up for new work. |
|
|
133
|
+
| `linear.statuses.inProgress` | `"In Progress"` | Status set after a workspace is provisioned; counts toward `maximumInProgress`. |
|
|
134
|
+
| `linear.statuses.done` | `"Done"` | Status that triggers worktree cleanup. |
|
|
135
|
+
| `linear.statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup, board remaining counts, and blocker checks. The `done` status is always included. |
|
|
136
|
+
| `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
|
|
137
|
+
| `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
|
|
138
|
+
| `workspace.projectDir` | **required** | Parent dir for cloned repos and local sibling ticket worktrees. Remote ticket worktrees live in `remote.worktreeRoot` on the remote runner. |
|
|
139
|
+
| `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored. |
|
|
140
|
+
| `orchestrator.maximumInProgress` | `4` | Cap on tickets in `linear.statuses.inProgress` at once. |
|
|
141
|
+
| `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
|
|
142
|
+
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
|
|
143
|
+
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew run --ticket <TICKET>` for unlabeled tickets. `crew run` without `--ticket` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
|
|
144
|
+
| `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
|
|
145
|
+
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Local macOS runs execute in the worktree through Safehouse/clearance; remote runs execute inside the remote worktree. `{{worktree}}` is replaced before launch and legacy `{{sandbox}}` expands to an empty string. |
|
|
146
|
+
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
|
|
147
|
+
| `models.definitions.<name>.usage` | optional | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage`. Omit to never gate. When `usage.codexbar.source` is omitted, groundcrew uses `auto` on macOS and `cli` elsewhere. |
|
|
148
|
+
| `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. See "Disabling a shipped default" below. |
|
|
149
|
+
| `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. |
|
|
150
|
+
| `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"tmux"` to fail loudly when the chosen backend is missing. tmux windows live in a dedicated `groundcrew` session. |
|
|
151
|
+
| `remote.provider` | `"sprite"` | Remote runner provider used for tickets labeled `agent-remote`. Sprite is currently the only provider. |
|
|
152
|
+
| `remote.runnerName` | `"crew-claude-1"` | Remote runner used for tickets labeled `agent-remote`. |
|
|
153
|
+
| `remote.owner` | `"ClipboardHealth"` | GitHub owner used when cloning bare repository names inside the remote runner. |
|
|
154
|
+
| `remote.repoRoot` | `"/home/sprite/dev"` | Shared clone root inside the remote runner. |
|
|
155
|
+
| `remote.worktreeRoot` | `"/home/sprite/groundcrew/worktrees"` | Per-ticket remote worktree root inside the remote runner. |
|
|
156
|
+
| `remote.secretNames` | `["NPM_TOKEN", "BUF_TOKEN"]` | Build-only env vars that may be uploaded for remote dependency setup, then unset before the agent process starts. |
|
|
157
|
+
| `logging.file` | XDG state path | Append-mode log file destination. `log()` / `logEvent()` tee here in addition to stdout, so a vanished workspace doesn't take the evidence with it. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
|
|
158
|
+
|
|
159
|
+
The branch prefix (`<prefix>-<TICKET>`) is derived from your OS username (`os.userInfo().username`), not configured. Agent selection looks for a top-level Linear label named `agent-<model>` (e.g. `agent-claude`, `agent-codex`). Add `agent-remote` to run that ticket in the configured remote runner instead of locally; `agent-remote` is a modifier label, not a model. **`crew run` without `--ticket` only fetches tickets with an `agent-*` label** — the GraphQL query filters them server-side, so unlabeled tickets are never returned by Linear's API and do not appear in the rendered board. Use `crew run --ticket <TICKET>` to provision an unlabeled ticket on demand (manual setup falls back to `models.default`). The reserved label `agent-any` routes the ticket to the configured model with the most available session capacity (lowest codexbar session-used percent), skipping any model already over `sessionLimitPercentage`. With no usage data, `agent-any` resolves to `models.default`. The name `any` cannot be used in `models.definitions`. Todo tickets blocked by Linear issues that are not in `linear.statuses.terminal` are skipped until their blockers reach a terminal status.
|
|
160
160
|
|
|
161
161
|
### Disabling a shipped default
|
|
162
162
|
|
|
@@ -202,7 +202,7 @@ crew setup repos [--dry-run] [<repo>...]
|
|
|
202
202
|
crew cleanup <TICKET>
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
-
`crew run --ticket <TICKET>` provisions a single ticket the same way the orchestrator would: the repo is parsed from the ticket's Linear description, the model comes from the ticket's `agent-*` label, and `agent-remote` is honored. If the description does not mention a repo from `workspace.knownRepositories`, setup fails before provisioning. `--watch` and `--ticket` are mutually exclusive — `--watch` drives the orchestrator loop; `--ticket` provisions one ticket and exits. `crew cleanup <TICKET>` resolves to every tracked worktree carrying that ticket id (host and remote kinds, across repos) and tears them all down. To inspect remote sessions, run `crew remote sessions` or pass an explicit runner name. To attach to a listed session id or command selector, run `crew remote attach <session-id-or-command>`. If an attached agent appears stuck in a long-running shell tool, use `crew remote ps <runner-name>` to find the child process group (`PGID`) under the agent, then use `crew remote interrupt <PGID> --runner <runner-name>` to send SIGINT to that child command without killing the agent session. With the Sprite provider, if cleanup cannot remove a remote worktree because the agent is still running, stop that session with `sprite sessions kill -s crew-claude-1 <session-id>` and retry cleanup. To inspect codexbar session windows directly, run `codexbar usage`; the orchestrator already gates on this internally via `orchestrator.sessionLimitPercentage`.
|
|
205
|
+
`crew run --ticket <TICKET>` provisions a single ticket the same way the orchestrator would: the repo is parsed from the ticket's Linear description, the model comes from the ticket's `agent-*` label, manual setup falls back to `models.default` for unlabeled tickets, and `agent-remote` is honored. If the description does not mention a repo from `workspace.knownRepositories`, setup fails before provisioning. `--watch` and `--ticket` are mutually exclusive — `--watch` drives the orchestrator loop; `--ticket` provisions one ticket and exits. `crew cleanup <TICKET>` resolves to every tracked worktree carrying that ticket id (host and remote kinds, across repos) and tears them all down. To inspect remote sessions, run `crew remote sessions` or pass an explicit runner name. To attach to a listed session id or command selector, run `crew remote attach <session-id-or-command>`. If an attached agent appears stuck in a long-running shell tool, use `crew remote ps <runner-name>` to find the child process group (`PGID`) under the agent, then use `crew remote interrupt <PGID> --runner <runner-name>` to send SIGINT to that child command without killing the agent session. With the Sprite provider, if cleanup cannot remove a remote worktree because the agent is still running, stop that session with `sprite sessions kill -s crew-claude-1 <session-id>` and retry cleanup. To inspect codexbar session windows directly, run `codexbar usage`; the orchestrator already gates on this internally via `orchestrator.sessionLimitPercentage`.
|
|
206
206
|
|
|
207
207
|
## Gotchas
|
|
208
208
|
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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,SAAS,CAAC,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;AAaD,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;AAw1BD,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC9F;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"}
|
|
@@ -14,7 +14,7 @@ const KNOWN_MCP_SERVER_URLS = {
|
|
|
14
14
|
const DEFAULT_CHECKPOINT_COMMENT = "groundcrew remote runner baseline: selected agent auth, git identity, and MCP config";
|
|
15
15
|
const CLAUDE_SUBSCRIPTION_LOGIN_FLAG = ["--claude", "ai"].join("");
|
|
16
16
|
const DEFAULT_REPOSITORY_OWNER = "ClipboardHealth";
|
|
17
|
-
const
|
|
17
|
+
const DEFAULT_GIT_REMOTE = "origin";
|
|
18
18
|
const DATADOG_PUP_VERSION = "0.63.0";
|
|
19
19
|
const DATADOG_OAUTH_CALLBACK_PORT = 8000;
|
|
20
20
|
const DATADOG_AUTH_STATUS_RETRY_ATTEMPTS = 5;
|
|
@@ -52,7 +52,7 @@ function usage() {
|
|
|
52
52
|
"",
|
|
53
53
|
"Bootstrap options:",
|
|
54
54
|
" --branch <branch> Checkout/create a ticket branch before installing deps",
|
|
55
|
-
" --base <branch> Base branch used when creating a missing branch (default:
|
|
55
|
+
" --base <branch> Base branch used when creating a missing branch (default: git.defaultBranch)",
|
|
56
56
|
" --owner <owner> GitHub owner for bare repo names (default: ClipboardHealth)",
|
|
57
57
|
" --secret <env-name> Forward one required build secret; repeat for multiple",
|
|
58
58
|
" --no-secrets Do not forward NPM_TOKEN/BUF_TOKEN even if present",
|
|
@@ -233,7 +233,7 @@ function parseBootstrapArguments(argv) {
|
|
|
233
233
|
}
|
|
234
234
|
validateRepository(repository);
|
|
235
235
|
let owner = DEFAULT_REPOSITORY_OWNER;
|
|
236
|
-
let baseBranch
|
|
236
|
+
let baseBranch;
|
|
237
237
|
let branchName;
|
|
238
238
|
let shouldUseSecrets = true;
|
|
239
239
|
let shouldRequireSelectedSecrets = false;
|
|
@@ -276,10 +276,10 @@ function parseBootstrapArguments(argv) {
|
|
|
276
276
|
runnerName,
|
|
277
277
|
repository,
|
|
278
278
|
owner,
|
|
279
|
-
baseBranch,
|
|
280
279
|
secretNames: selectedSecretNames.length > 0 ? selectedSecretNames : [...BUILD_SECRET_NAMES],
|
|
281
280
|
shouldRequireSelectedSecrets,
|
|
282
281
|
shouldUseSecrets,
|
|
282
|
+
...(baseBranch === undefined ? {} : { baseBranch }),
|
|
283
283
|
...(branchName === undefined ? {} : { branchName }),
|
|
284
284
|
};
|
|
285
285
|
}
|
|
@@ -634,34 +634,46 @@ function repositoryDirectoryName(repository) {
|
|
|
634
634
|
function remoteBootstrapCommand(options) {
|
|
635
635
|
const slug = repositorySlug(options);
|
|
636
636
|
const directoryName = repositoryDirectoryName(options.repository);
|
|
637
|
+
const baseRef = `${options.gitRemote}/${options.baseBranch}`;
|
|
637
638
|
const unsetSecretsLine = options.secretNames.length === 0 ? ":" : `unset ${options.secretNames.join(" ")}`;
|
|
638
639
|
const checkoutLines = options.branchName === undefined
|
|
639
|
-
? [
|
|
640
|
-
`git checkout -B ${shellSingleQuote(options.baseBranch)} ${shellSingleQuote(`origin/${options.baseBranch}`)}`,
|
|
641
|
-
]
|
|
640
|
+
? [`git checkout -B ${shellSingleQuote(options.baseBranch)} ${shellSingleQuote(baseRef)}`]
|
|
642
641
|
: [
|
|
643
|
-
`if git show-ref --verify --quiet ${shellSingleQuote(`refs/remotes
|
|
644
|
-
` git checkout -B ${shellSingleQuote(options.branchName)} ${shellSingleQuote(
|
|
642
|
+
`if git show-ref --verify --quiet ${shellSingleQuote(`refs/remotes/${options.gitRemote}/${options.branchName}`)}; then`,
|
|
643
|
+
` git checkout -B ${shellSingleQuote(options.branchName)} ${shellSingleQuote(`${options.gitRemote}/${options.branchName}`)}`,
|
|
645
644
|
"else",
|
|
646
|
-
` git checkout -B ${shellSingleQuote(options.branchName)} ${shellSingleQuote(
|
|
645
|
+
` git checkout -B ${shellSingleQuote(options.branchName)} ${shellSingleQuote(baseRef)}`,
|
|
647
646
|
"fi",
|
|
648
647
|
];
|
|
649
648
|
return [
|
|
650
649
|
"set -euo pipefail",
|
|
651
650
|
`cleanup() { rm -f ${shellSingleQuote(REMOTE_SECRETS_FILE)}; ${unsetSecretsLine}; }`,
|
|
652
651
|
"trap cleanup EXIT",
|
|
652
|
+
`git_remote=${shellSingleQuote(options.gitRemote)}`,
|
|
653
653
|
'mkdir -p "$HOME/dev"',
|
|
654
654
|
`repo_dir="$HOME/dev/${directoryName}"`,
|
|
655
655
|
'if [ ! -d "$repo_dir/.git" ]; then',
|
|
656
656
|
` gh repo clone ${shellSingleQuote(slug)} "$repo_dir"`,
|
|
657
657
|
"fi",
|
|
658
658
|
'cd "$repo_dir"',
|
|
659
|
-
|
|
659
|
+
'if ! git remote get-url "$git_remote" >/dev/null 2>&1; then',
|
|
660
|
+
' origin_url="$(git remote get-url origin)"',
|
|
661
|
+
' git remote add "$git_remote" "$origin_url"',
|
|
662
|
+
"fi",
|
|
663
|
+
'git fetch "$git_remote" --prune',
|
|
660
664
|
...checkoutLines,
|
|
661
665
|
`if [ -f ${shellSingleQuote(REMOTE_SECRETS_FILE)} ]; then set -a && . ${shellSingleQuote(REMOTE_SECRETS_FILE)} && set +a; fi`,
|
|
662
666
|
DEFAULT_REMOTE_SETUP_COMMAND,
|
|
663
667
|
].join("\n");
|
|
664
668
|
}
|
|
669
|
+
async function resolveBootstrapOptions(options) {
|
|
670
|
+
const config = await loadConfig();
|
|
671
|
+
return {
|
|
672
|
+
...options,
|
|
673
|
+
baseBranch: options.baseBranch ?? config.git.defaultBranch,
|
|
674
|
+
gitRemote: config.git.remote,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
665
677
|
function noSecretCleanup() {
|
|
666
678
|
// No staged secrets file was created.
|
|
667
679
|
}
|
|
@@ -697,12 +709,16 @@ function stageBuildSecrets(options) {
|
|
|
697
709
|
};
|
|
698
710
|
}
|
|
699
711
|
export async function bootstrapRemoteRepository(options) {
|
|
712
|
+
const bootstrapOptions = {
|
|
713
|
+
...options,
|
|
714
|
+
gitRemote: options.gitRemote ?? DEFAULT_GIT_REMOTE,
|
|
715
|
+
};
|
|
700
716
|
const config = remoteConfigWithRunnerName(options.runnerName);
|
|
701
717
|
const provider = providerFor(config);
|
|
702
718
|
if (!(await provider.runnerExists(config))) {
|
|
703
719
|
throw new Error(`Remote runner ${options.runnerName} does not exist. Run crew remote setup first.`);
|
|
704
720
|
}
|
|
705
|
-
const stagedSecrets = stageBuildSecrets(
|
|
721
|
+
const stagedSecrets = stageBuildSecrets(bootstrapOptions);
|
|
706
722
|
try {
|
|
707
723
|
if (stagedSecrets.names.length > 0) {
|
|
708
724
|
log(`Forwarding build secret names for setup only: ${stagedSecrets.names.join(", ")}`);
|
|
@@ -710,11 +726,11 @@ export async function bootstrapRemoteRepository(options) {
|
|
|
710
726
|
const files = stagedSecrets.filePath === undefined
|
|
711
727
|
? []
|
|
712
728
|
: [{ localPath: stagedSecrets.filePath, remotePath: REMOTE_SECRETS_FILE }];
|
|
713
|
-
log(`Bootstrapping ${repositorySlug(
|
|
729
|
+
log(`Bootstrapping ${repositorySlug(bootstrapOptions)} in ${options.runnerName}`);
|
|
714
730
|
await provider.runCommand({
|
|
715
731
|
config,
|
|
716
732
|
files,
|
|
717
|
-
remoteArguments: ["bash", "-lc", remoteBootstrapCommand(
|
|
733
|
+
remoteArguments: ["bash", "-lc", remoteBootstrapCommand(bootstrapOptions)],
|
|
718
734
|
options: { stdio: "inherit", timeoutMs: 0 },
|
|
719
735
|
});
|
|
720
736
|
}
|
|
@@ -790,7 +806,7 @@ export async function remoteCli(argv) {
|
|
|
790
806
|
return;
|
|
791
807
|
}
|
|
792
808
|
if (action === "bootstrap") {
|
|
793
|
-
await bootstrapRemoteRepository(parseBootstrapArguments(rest));
|
|
809
|
+
await bootstrapRemoteRepository(await resolveBootstrapOptions(parseBootstrapArguments(rest)));
|
|
794
810
|
return;
|
|
795
811
|
}
|
|
796
812
|
if (action === "sessions") {
|
|
@@ -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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"}
|
|
@@ -105,6 +105,9 @@ function worktreeTicketComponent(ticket) {
|
|
|
105
105
|
}
|
|
106
106
|
function spriteCreateWorktreeCommand(arguments_) {
|
|
107
107
|
const slug = repositorySlug(arguments_.owner, arguments_.repository);
|
|
108
|
+
const branchRemoteRef = `refs/remotes/${arguments_.gitRemote}/${arguments_.branchName}`;
|
|
109
|
+
const branchRef = `${arguments_.gitRemote}/${arguments_.branchName}`;
|
|
110
|
+
const baseRef = `${arguments_.gitRemote}/${arguments_.baseBranch}`;
|
|
108
111
|
return [
|
|
109
112
|
"set -euo pipefail",
|
|
110
113
|
`repo_root=${shellSingleQuote(arguments_.repoRoot)}`,
|
|
@@ -112,20 +115,27 @@ function spriteCreateWorktreeCommand(arguments_) {
|
|
|
112
115
|
`repo_dir=${shellSingleQuote(arguments_.repoDir)}`,
|
|
113
116
|
`worktree_dir=${shellSingleQuote(arguments_.worktreeDir)}`,
|
|
114
117
|
`branch=${shellSingleQuote(arguments_.branchName)}`,
|
|
115
|
-
`
|
|
118
|
+
`git_remote=${shellSingleQuote(arguments_.gitRemote)}`,
|
|
119
|
+
`branch_remote_ref=${shellSingleQuote(branchRemoteRef)}`,
|
|
120
|
+
`branch_ref=${shellSingleQuote(branchRef)}`,
|
|
121
|
+
`base_ref=${shellSingleQuote(baseRef)}`,
|
|
116
122
|
'mkdir -p "$repo_root" "$worktree_root"',
|
|
117
123
|
'if [ ! -d "$repo_dir/.git" ]; then',
|
|
118
124
|
` gh repo clone ${shellSingleQuote(slug)} "$repo_dir"`,
|
|
119
125
|
"fi",
|
|
120
|
-
'git -C "$repo_dir"
|
|
126
|
+
'if ! git -C "$repo_dir" remote get-url "$git_remote" >/dev/null 2>&1; then',
|
|
127
|
+
' origin_url="$(git -C "$repo_dir" remote get-url origin)"',
|
|
128
|
+
' git -C "$repo_dir" remote add "$git_remote" "$origin_url"',
|
|
129
|
+
"fi",
|
|
130
|
+
'git -C "$repo_dir" fetch "$git_remote" --prune',
|
|
121
131
|
'if [ -e "$worktree_dir" ]; then',
|
|
122
132
|
' echo "Remote worktree already exists: $worktree_dir" >&2',
|
|
123
133
|
" exit 1",
|
|
124
134
|
"fi",
|
|
125
|
-
'if git -C "$repo_dir" show-ref --verify --quiet "
|
|
126
|
-
' git -C "$repo_dir" worktree add -B "$branch" "$worktree_dir" "
|
|
135
|
+
'if git -C "$repo_dir" show-ref --verify --quiet "$branch_remote_ref"; then',
|
|
136
|
+
' git -C "$repo_dir" worktree add -B "$branch" "$worktree_dir" "$branch_ref"',
|
|
127
137
|
"else",
|
|
128
|
-
' git -C "$repo_dir" worktree add -b "$branch" "$worktree_dir" "
|
|
138
|
+
' git -C "$repo_dir" worktree add -b "$branch" "$worktree_dir" "$base_ref"',
|
|
129
139
|
"fi",
|
|
130
140
|
].join("\n");
|
|
131
141
|
}
|
|
@@ -306,7 +316,7 @@ export const spriteRemoteRunnerProvider = {
|
|
|
306
316
|
});
|
|
307
317
|
},
|
|
308
318
|
async createWorktree(arguments_) {
|
|
309
|
-
const { config, repository, ticket, branchName, baseBranch, signal } = arguments_;
|
|
319
|
+
const { config, repository, ticket, branchName, baseBranch, gitRemote, signal } = arguments_;
|
|
310
320
|
const remoteRepositoryName = repositoryDirectoryName(config.owner, repository);
|
|
311
321
|
const remoteRepoDir = remotePathJoin(config.repoRoot, remoteRepositoryName);
|
|
312
322
|
const remoteWorktreeDir = remotePathJoin(config.worktreeRoot, `${remoteRepositoryName}-${worktreeTicketComponent(ticket)}`);
|
|
@@ -324,6 +334,7 @@ export const spriteRemoteRunnerProvider = {
|
|
|
324
334
|
worktreeDir: remoteWorktreeDir,
|
|
325
335
|
branchName,
|
|
326
336
|
baseBranch,
|
|
337
|
+
gitRemote,
|
|
327
338
|
repoRoot: config.repoRoot,
|
|
328
339
|
worktreeRoot: config.worktreeRoot,
|
|
329
340
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiBH,OAAO,EAA8B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAMpG,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE7C,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;IACnB,mCAAmC;IACnC,cAAc,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,CAAC;IACtD,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAaD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAW7E;AAgdD,4FAA4F;AAC5F,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAS,YAAY,CACnB,MAAM,EAAE,cAAc,EACtB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,aAAa,GAAG,SAAS,CAI3B;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAUxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAKD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAkDzB;AAED,eAAO,MAAM,SAAS;;;;;;;CAOrB,CAAC"}
|
package/dist/lib/worktrees.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* pick the adapter by `spec.runner` (for `create`) or `entry.kind` (for
|
|
11
11
|
* `remove`), mirroring the `workspaces` module's adapter pattern.
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
14
15
|
import { homedir, userInfo } from "node:os";
|
|
15
16
|
import { dirname, resolve } from "node:path";
|
|
16
17
|
import { runCommandAsync } from "./commandRunner.js";
|
|
@@ -197,6 +198,23 @@ function stateBaseDir() {
|
|
|
197
198
|
function remoteStateFilePath() {
|
|
198
199
|
return resolve(stateBaseDir(), "groundcrew", "remote-worktrees.json");
|
|
199
200
|
}
|
|
201
|
+
function normalizeRemoteStateNamespacePath(path) {
|
|
202
|
+
const normalized = path.replace(/\/+$/u, "");
|
|
203
|
+
return normalized.length === 0 ? "/" : normalized;
|
|
204
|
+
}
|
|
205
|
+
function remoteStateNamespaceFor(config) {
|
|
206
|
+
return JSON.stringify({
|
|
207
|
+
version: 1,
|
|
208
|
+
projectDir: resolve(config.workspace.projectDir),
|
|
209
|
+
remote: {
|
|
210
|
+
provider: config.remote.provider,
|
|
211
|
+
runnerName: config.remote.runnerName,
|
|
212
|
+
owner: config.remote.owner,
|
|
213
|
+
repoRoot: normalizeRemoteStateNamespacePath(config.remote.repoRoot),
|
|
214
|
+
worktreeRoot: normalizeRemoteStateNamespacePath(config.remote.worktreeRoot),
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
200
218
|
function isRemoteEntry(value) {
|
|
201
219
|
if (typeof value !== "object" || value === null) {
|
|
202
220
|
return false;
|
|
@@ -209,7 +227,8 @@ function isRemoteEntry(value) {
|
|
|
209
227
|
entry.kind === "remote" &&
|
|
210
228
|
isRemoteRunnerProviderName(entry.remoteProvider) &&
|
|
211
229
|
typeof entry.remoteRunnerName === "string" &&
|
|
212
|
-
typeof entry.remoteRepoDir === "string"
|
|
230
|
+
typeof entry.remoteRepoDir === "string" &&
|
|
231
|
+
(entry.remoteStateNamespace === undefined || typeof entry.remoteStateNamespace === "string"));
|
|
213
232
|
}
|
|
214
233
|
function readRemoteEntries() {
|
|
215
234
|
try {
|
|
@@ -222,21 +241,87 @@ function readRemoteEntries() {
|
|
|
222
241
|
}
|
|
223
242
|
function writeRemoteEntries(entries) {
|
|
224
243
|
const path = remoteStateFilePath();
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
const directory = dirname(path);
|
|
245
|
+
mkdirSync(directory, { recursive: true });
|
|
246
|
+
const temporaryPath = resolve(directory, `.remote-worktrees-${process.pid}-${randomUUID()}.tmp`);
|
|
247
|
+
let didReplaceStateFile = false;
|
|
248
|
+
try {
|
|
249
|
+
writeFileSync(temporaryPath, `${JSON.stringify({ entries }, undefined, 2)}\n`);
|
|
250
|
+
renameSync(temporaryPath, path);
|
|
251
|
+
didReplaceStateFile = true;
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
if (!didReplaceStateFile) {
|
|
255
|
+
removeTemporaryRemoteStateFileBestEffort(temporaryPath);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function remoteStateEntryFor(namespace, entry) {
|
|
260
|
+
return { ...entry, remoteStateNamespace: namespace };
|
|
227
261
|
}
|
|
228
|
-
function
|
|
229
|
-
|
|
262
|
+
function remoteStateEntryMatchesNamespace(namespace, entry) {
|
|
263
|
+
// Legacy records have no project/config identity. Keep them on disk, but do
|
|
264
|
+
// not expose them to cleanup paths that could remove another config's remote
|
|
265
|
+
// worktree.
|
|
266
|
+
return entry.remoteStateNamespace === namespace;
|
|
230
267
|
}
|
|
231
|
-
function
|
|
232
|
-
|
|
268
|
+
function remoteStateEntryMatchesWorktree(namespace, candidate, entry) {
|
|
269
|
+
return (candidate.remoteStateNamespace === namespace &&
|
|
270
|
+
candidate.repository === entry.repository &&
|
|
233
271
|
candidate.ticket === entry.ticket &&
|
|
234
272
|
candidate.dir === entry.dir &&
|
|
235
|
-
candidate.kind === "remote")
|
|
273
|
+
candidate.kind === "remote");
|
|
274
|
+
}
|
|
275
|
+
function worktreeEntryFromRemoteState(entry) {
|
|
276
|
+
return {
|
|
277
|
+
repository: entry.repository,
|
|
278
|
+
ticket: entry.ticket,
|
|
279
|
+
branchName: entry.branchName,
|
|
280
|
+
dir: entry.dir,
|
|
281
|
+
kind: entry.kind,
|
|
282
|
+
remoteProvider: entry.remoteProvider,
|
|
283
|
+
remoteRunnerName: entry.remoteRunnerName,
|
|
284
|
+
remoteRepoDir: entry.remoteRepoDir,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function upsertRemoteEntry(config, entry) {
|
|
288
|
+
const namespace = remoteStateNamespaceFor(config);
|
|
289
|
+
const stateEntry = remoteStateEntryFor(namespace, entry);
|
|
290
|
+
writeRemoteEntries([
|
|
291
|
+
...readRemoteEntries().filter((candidate) => !remoteStateEntryMatchesWorktree(namespace, candidate, entry)),
|
|
292
|
+
stateEntry,
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
function removeTemporaryRemoteStateFileBestEffort(path) {
|
|
296
|
+
try {
|
|
297
|
+
rmSync(path, { force: true });
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
/* v8 ignore next @preserve -- only external filesystem races can make best-effort temp cleanup fail */
|
|
301
|
+
log(`Temporary remote state file cleanup skipped: ${errorMessage(error)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function deleteRemoteEntry(config, entry) {
|
|
305
|
+
const namespace = remoteStateNamespaceFor(config);
|
|
306
|
+
writeRemoteEntries(readRemoteEntries().filter((candidate) => !remoteStateEntryMatchesWorktree(namespace, candidate, entry)));
|
|
236
307
|
}
|
|
237
308
|
function remoteProviderFor(config, entry) {
|
|
238
309
|
return getRemoteRunnerProvider(entry?.remoteProvider ?? config.remote.provider);
|
|
239
310
|
}
|
|
311
|
+
async function removeCreatedRemoteWorktreeBestEffort(arguments_) {
|
|
312
|
+
try {
|
|
313
|
+
log(`Rolling back remote worktree ${arguments_.entry.dir} after local state persistence failed...`);
|
|
314
|
+
await arguments_.provider.removeWorktree({
|
|
315
|
+
config: arguments_.config.remote,
|
|
316
|
+
entry: arguments_.entry,
|
|
317
|
+
force: true,
|
|
318
|
+
...signalProperty(arguments_.signal),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
log(`Remote worktree rollback skipped after local state persistence failed: ${errorMessage(error)}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
240
325
|
const remoteWorktreeAdapter = {
|
|
241
326
|
async create(config, spec, signal) {
|
|
242
327
|
const base = basePaths(config, spec.repository, spec.ticket);
|
|
@@ -248,6 +333,7 @@ const remoteWorktreeAdapter = {
|
|
|
248
333
|
ticket: spec.ticket,
|
|
249
334
|
branchName: base.branchName,
|
|
250
335
|
baseBranch: config.git.defaultBranch,
|
|
336
|
+
gitRemote: config.git.remote,
|
|
251
337
|
...signalProperty(signal),
|
|
252
338
|
});
|
|
253
339
|
const entry = {
|
|
@@ -260,11 +346,26 @@ const remoteWorktreeAdapter = {
|
|
|
260
346
|
remoteRunnerName: config.remote.runnerName,
|
|
261
347
|
remoteRepoDir,
|
|
262
348
|
};
|
|
263
|
-
|
|
349
|
+
try {
|
|
350
|
+
upsertRemoteEntry(config, entry);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
await removeCreatedRemoteWorktreeBestEffort({
|
|
354
|
+
config,
|
|
355
|
+
provider,
|
|
356
|
+
entry,
|
|
357
|
+
...signalProperty(signal),
|
|
358
|
+
});
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
264
361
|
return entry;
|
|
265
362
|
},
|
|
266
363
|
list(config) {
|
|
267
|
-
|
|
364
|
+
const namespace = remoteStateNamespaceFor(config);
|
|
365
|
+
return readRemoteEntries()
|
|
366
|
+
.filter((entry) => remoteStateEntryMatchesNamespace(namespace, entry))
|
|
367
|
+
.filter((entry) => config.workspace.knownRepositories.includes(entry.repository))
|
|
368
|
+
.map(worktreeEntryFromRemoteState);
|
|
268
369
|
},
|
|
269
370
|
async remove(config, entry, options) {
|
|
270
371
|
log(`Removing remote worktree ${entry.dir}${options.force ? " (--force)" : ""}...`);
|
|
@@ -279,7 +380,7 @@ const remoteWorktreeAdapter = {
|
|
|
279
380
|
force: options.force,
|
|
280
381
|
...signalProperty(options.signal),
|
|
281
382
|
});
|
|
282
|
-
deleteRemoteEntry(entry);
|
|
383
|
+
deleteRemoteEntry(config, entry);
|
|
283
384
|
},
|
|
284
385
|
};
|
|
285
386
|
function adapterForEntry(entry) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.2",
|
|
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",
|