@clipboard-health/groundcrew 4.9.0 → 4.10.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
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- Dispatch your ticket backlog to AI coding agents. One git worktree per ticket, sandboxed by default.
9
+ Dispatch your ticket backlog to local, interactive AI coding agents. One git worktree per ticket, sandboxed by default.
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -17,16 +17,22 @@
17
17
  </p>
18
18
 
19
19
  <p align="center">
20
- <img alt="Groundcrew picking up tickets and running coding agents in parallel" src="./static/demo.gif" width="800">
20
+ <a href="./static/demo.tape"><img alt="Groundcrew dispatching tickets into tmux panes with coding agents running in parallel" src="./static/demo.gif" width="800"></a>
21
+ </p>
22
+
23
+ <p align="center">
24
+ VHS source: <a href="./static/demo.tape">static/demo.tape</a>.
21
25
  </p>
22
26
 
23
27
  Groundcrew watches assigned tickets, creates isolated worktrees, launches agent CLIs in dedicated terminals, and leaves each ticket's work on its own PR-ready branch. For the backstory, read _[Tickets to pull requests while you sleep](https://www.clipboardworks.com/resources/blog/tickets-to-pull-requests-while-you-sleep)_.
24
28
 
25
29
  ## Why
26
30
 
31
+ - **Local.** Agents run on your machine with your tools, shell, and credentials. That makes them more steerable than remote agents, and easy to nudge when they drift.
32
+ - **Interactive.** Each ticket launches the real `claude` or `codex` CLI in its own terminal pane, not a wrapper that approximates it. Watch any session live and take over when you need to.
27
33
  - **One worktree per ticket.** Agents work in parallel without stepping on each other.
34
+ - **Sandboxed by default.** Safehouse or Docker Sandboxes isolate each agent on the host; `none` is an explicit escape hatch.
28
35
  - **Pluggable ticket sources.** Linear by default; Jira and local files via [ticket sources](./docs/ticket-sources.md).
29
- - **Local-first isolation.** Safehouse, Docker Sandboxes, or an explicit `none` escape hatch.
30
36
  - **Multi-agent routing.** Ships `claude` and `codex` presets; bring your own CLI in config.
31
37
 
32
38
  ## Prerequisites
@@ -83,8 +89,7 @@ crew init [--global | --local] [--force] [--dry-run] # create a crew.config.
83
89
  [--runner <auto|safehouse|sdx|none>] [--model <claude|codex>]
84
90
  crew doctor # check setup
85
91
  crew status [<TICKET>] # inspect current state or one ticket
86
- crew run # one-shot orchestration
87
- crew run --watch # poll forever
92
+ crew run [--watch] # one-shot or --watch forever
88
93
  crew start <TICKET> # provision + launch one ticket now
89
94
  crew stop <TICKET> [--reason <text>] # stop workspace, keep worktree
90
95
  crew resume <TICKET> # reopen a paused ticket
@@ -106,15 +111,18 @@ export default {
106
111
  projectDir: "~/dev",
107
112
  knownRepositories: ["OWNER/REPO"],
108
113
  },
109
- local: {
110
- runner: "auto",
111
- },
112
114
  models: {
113
115
  default: "claude",
114
116
  definitions: {
115
117
  claude: {},
116
118
  },
117
119
  },
120
+ defaults: {
121
+ hooks: {
122
+ // No-op placeholder; replace with your repo's setup, e.g. "npm ci".
123
+ prepareWorktree: "true",
124
+ },
125
+ },
118
126
  } satisfies Config;
119
127
  ```
120
128
 
@@ -143,6 +151,12 @@ node --run crew:op -- run --watch
143
151
 
144
152
  Both forms discover config through cosmiconfig. Source edits in `src/**` are picked up on the next invocation. Requires Node >= 24.
145
153
 
154
+ Regenerate the README demo with VHS:
155
+
156
+ ```bash
157
+ ./static/render-demo.sh
158
+ ```
159
+
146
160
  ## License
147
161
 
148
162
  [MIT](./LICENSE)
@@ -64,8 +64,9 @@ raw.githubusercontent.com
64
64
  release-assets.githubusercontent.com
65
65
  results-receiver.actions.githubusercontent.com
66
66
 
67
- # npm registry + package website
67
+ # npm/Conda registries + package websites
68
68
  api.npmjs.org
69
+ conda.anaconda.org
69
70
  registry.npmjs.org
70
71
  www.npmjs.com
71
72
 
@@ -83,6 +84,9 @@ formulae.brew.sh
83
84
  hub.docker.com
84
85
  index.docker.io
85
86
  json.schemastore.org
87
+ mise-versions.jdx.dev
86
88
  nx.dev
89
+ playwright.azureedge.net
90
+ registry.terraform.io
87
91
  sourcegraph.com
88
92
  vitest.dev
@@ -35,6 +35,15 @@ export default {
35
35
  // },
36
36
  },
37
37
  },
38
+ // Repo-preparation hook: runs after each worktree is created and before the
39
+ // agent launches. The default below is a no-op placeholder. Replace it with
40
+ // your repo's setup, e.g. "npm ci" or "uv sync --dev --frozen". A repo-local
41
+ // `.groundcrew/config.json` hooks.prepareWorktree overrides this per repo.
42
+ defaults: {
43
+ hooks: {
44
+ prepareWorktree: "true",
45
+ },
46
+ },
38
47
  // Everything below is optional — defaults shown for reference. Uncomment
39
48
  // and edit to override.
40
49
  //
@@ -59,14 +68,6 @@ export default {
59
68
  //
60
69
  // git: { remote: "origin", defaultBranch: "main" },
61
70
  //
62
- // // Fallback repo-preparation hook for repos that do not define
63
- // // `.groundcrew/config.json` hooks.prepareWorktree. Repo-local config wins.
64
- // defaults: {
65
- // hooks: {
66
- // prepareWorktree: "test ! -f package-lock.json || npm ci",
67
- // },
68
- // },
69
- //
70
71
  // orchestrator: {
71
72
  // maximumInProgress: 4,
72
73
  // pollIntervalMilliseconds: 120_000,
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuKH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA8E/C"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8KH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAgF/C"}
@@ -5,7 +5,7 @@
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { createBoard } from "../lib/board.js";
7
7
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
8
- import { loadConfig, } from "../lib/config.js";
8
+ import { loadConfigWithSource, } from "../lib/config.js";
9
9
  import { detectHostCapabilities, which } from "../lib/host.js";
10
10
  import { resolveLocalRunner } from "../lib/localRunner.js";
11
11
  import { gatedModels } from "../lib/usage.js";
@@ -15,6 +15,11 @@ import { resolveWorkspaceKind } from "../lib/workspaces.js";
15
15
  // catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
16
16
  const MAX_TOKENS_PER_CMD = 2;
17
17
  const BUILT_IN_MODEL_NAMES = ["claude", "codex"];
18
+ const CONFIG_SOURCE_LABELS = {
19
+ env: "GROUNDCREW_CONFIG",
20
+ project: "project",
21
+ xdg: "global XDG",
22
+ };
18
23
  async function checkCmd(cmd, required, hint) {
19
24
  const path = await which(cmd);
20
25
  const resolvedHint = path ?? hint;
@@ -148,8 +153,10 @@ export async function doctor() {
148
153
  writeOutput("=================");
149
154
  let config;
150
155
  try {
151
- config = await loadConfig();
152
- writeOutput("[ok] config loaded");
156
+ const { config: loadedConfig, source } = await loadConfigWithSource();
157
+ config = loadedConfig;
158
+ const sourceLabel = CONFIG_SOURCE_LABELS[source.kind];
159
+ writeOutput(`[ok] config loaded — ${source.filepath} (${sourceLabel})`);
153
160
  }
154
161
  catch (error) {
155
162
  writeOutput(`[--] config: ${errorMessage(error)}`);
@@ -241,6 +241,15 @@ export interface ResolvedConfig {
241
241
  file: string;
242
242
  };
243
243
  }
244
+ export type ConfigSourceKind = "env" | "project" | "xdg";
245
+ export interface ConfigSource {
246
+ kind: ConfigSourceKind;
247
+ filepath: string;
248
+ }
249
+ export interface LoadedConfig {
250
+ config: Readonly<ResolvedConfig>;
251
+ source: Readonly<ConfigSource>;
252
+ }
244
253
  /**
245
254
  * Single source of truth for "is preLaunchEnv asking us to forward anything?"
246
255
  *
@@ -258,5 +267,6 @@ export declare function hasPreLaunchEnv(definition: Pick<ModelDefinition, "preLa
258
267
  * not enabled from an arbitrary unknown label like `agent-typo`.
259
268
  */
260
269
  export declare function isBuiltInModelNotEnabled(config: Pick<ResolvedConfig, "models">, name: string): boolean;
270
+ export declare function loadConfigWithSource(): Promise<Readonly<LoadedConfig>>;
261
271
  export declare function loadConfig(): Promise<Readonly<ResolvedConfig>>;
262
272
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -606,26 +606,28 @@ async function discoverUserConfig() {
606
606
  if (!existsSync(overridePath)) {
607
607
  fail(`GROUNDCREW_CONFIG=${overridePath} not found`);
608
608
  }
609
- return await loadAt(overridePath);
609
+ const result = await loadAt(overridePath);
610
+ return { result, source: { kind: "env", filepath: result.filepath } };
610
611
  }
611
612
  const project = await explorer.search(process.cwd());
612
613
  if (project !== null && project.isEmpty !== true) {
613
- return project;
614
+ return { result: project, source: { kind: "project", filepath: project.filepath } };
614
615
  }
615
616
  const xdgPath = findXdgConfigFile();
616
617
  if (xdgPath !== undefined) {
617
- return await loadAt(xdgPath);
618
+ const result = await loadAt(xdgPath);
619
+ return { result, source: { kind: "xdg", filepath: result.filepath } };
618
620
  }
619
621
  // Throw directly so oxlint's `consistent-return` rule sees a
620
622
  // terminating statement; it doesn't track `fail()`'s `never` return.
621
623
  throw new Error(`groundcrew config: no crew config found. Create crew.config.ts in your project root, or ${xdgConfigPath("groundcrew", "crew.config.ts")}, or set GROUNDCREW_CONFIG.`);
622
624
  }
623
625
  let cached;
624
- export async function loadConfig() {
626
+ export async function loadConfigWithSource() {
625
627
  if (cached) {
626
628
  return cached;
627
629
  }
628
- const result = await discoverUserConfig();
630
+ const { result, source } = await discoverUserConfig();
629
631
  const { filepath, isEmpty } = result;
630
632
  const userConfig = result.config;
631
633
  if (isEmpty === true || !isPlainObject(userConfig)) {
@@ -636,6 +638,13 @@ export async function loadConfig() {
636
638
  const resolved = applyDefaults(userConfig);
637
639
  validate(resolved);
638
640
  setLogFile(resolved.logging.file);
639
- cached = Object.freeze(resolved);
641
+ cached = Object.freeze({
642
+ config: Object.freeze(resolved),
643
+ source: Object.freeze(source),
644
+ });
640
645
  return cached;
641
646
  }
647
+ export async function loadConfig() {
648
+ const loadedConfig = await loadConfigWithSource();
649
+ return loadedConfig.config;
650
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",
@@ -84,7 +84,7 @@
84
84
  "@typescript/native-preview": "7.0.0-dev.20260527.2",
85
85
  "@vitest/coverage-v8": "4.1.7",
86
86
  "cspell": "10.0.0",
87
- "dependency-cruiser": "17.4.2",
87
+ "dependency-cruiser": "17.4.3",
88
88
  "husky": "9.1.7",
89
89
  "jscpd": "4.2.4",
90
90
  "knip": "6.14.2",
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Semantic ANSI colors. Actual hues are defined by the VHS theme in demo.tape,
5
+ # so content and chrome stay in sync: green = orchestrator, yellow = codex,
6
+ # blue = claude, bright-black = dim chrome.
7
+ c_reset=$'\033[0m'
8
+ c_dim=$'\033[90m'
9
+ c_green=$'\033[32m'
10
+ c_amber=$'\033[33m'
11
+ c_blue=$'\033[34m'
12
+
13
+ export PS1="\[${c_green}\]groundcrew\[${c_dim}\] on \[${c_reset}\]main \[${c_green}\]\$\[${c_reset}\] "
14
+
15
+ tmux rename-window 'crew run --watch'
16
+ tmux set-option -g status off
17
+ tmux set-option -g pane-border-status top
18
+ tmux set-option -g pane-border-format ' #{?#{m:codex*,#{pane_title}},#[fg=#fbbf24],#{?#{m:claude*,#{pane_title}},#[fg=#60a5fa],#[fg=#77d94e]}}#[bold]#{pane_title}#[default] '
19
+ tmux set-option -g pane-border-style 'fg=#3f3f46'
20
+ tmux set-option -g pane-active-border-style 'fg=#77d94e'
21
+ tmux set-option -g remain-on-exit on
22
+
23
+ printf '\033]2;groundcrew\033\\'
24
+
25
+ demo_agent_script="$(mktemp "${TMPDIR:-/tmp}/groundcrew-vhs-agent.XXXXXX")"
26
+ trap 'rm -f "${demo_agent_script}"' EXIT
27
+ cat >"${demo_agent_script}" <<'SH'
28
+ #!/usr/bin/env bash
29
+ set -euo pipefail
30
+
31
+ ticket="${1}"
32
+ model="${2}"
33
+ title="${3}"
34
+ worktree="${4:-groundcrew-${ticket}}"
35
+ branch="groundcrew/${ticket}"
36
+
37
+ reset=$'\033[0m'
38
+ bold=$'\033[1m'
39
+ dim=$'\033[90m'
40
+ green=$'\033[32m'
41
+ case "${model}" in
42
+ codex) accent=$'\033[33m' ;;
43
+ claude) accent=$'\033[34m' ;;
44
+ *) accent=$'\033[32m' ;;
45
+ esac
46
+
47
+ printf '\033]2;%s %s\033\\' "${model}" "${ticket}"
48
+ printf '%s%s%s clipboard/groundcrew\n' "${dim}" 'repo' "${reset}"
49
+ printf '%s%s%s %s\n\n' "${dim}" 'branch' "${reset}" "${branch}"
50
+ sleep 0.4
51
+ printf '%s$%s %s%s%s %s< prompts/%s.txt%s\n\n' "${dim}" "${reset}" "${accent}" "${model}" "${reset}" "${dim}" "${ticket}" "${reset}"
52
+ sleep 0.5
53
+ printf '%sTicket%s %s\n' "${dim}" "${reset}" "${title}"
54
+ sleep 0.4
55
+ printf '%sWorktree%s %s~/dev/c/%s%s\n\n' "${dim}" "${reset}" "${dim}" "${worktree}" "${reset}"
56
+ sleep 0.5
57
+ printf '%sReading repo context...%s\n' "${dim}" "${reset}"
58
+ sleep 0.5
59
+ printf '%sEditing in isolated branch...%s\n' "${dim}" "${reset}"
60
+ sleep 0.5
61
+ printf '%sRunning verification...%s\n' "${dim}" "${reset}"
62
+ sleep 0.5
63
+ printf '%s%s✓ Ready for review.%s\n' "${green}" "${bold}" "${reset}"
64
+
65
+ while :; do
66
+ sleep 60
67
+ done
68
+ SH
69
+ chmod +x "${demo_agent_script}"
70
+
71
+ demo_ts_second=18
72
+
73
+ demo_log() {
74
+ printf '%s[15:23:%02d]%s %s\n' "${c_dim}" "${demo_ts_second}" "${c_reset}" "${1}"
75
+ demo_ts_second=$((demo_ts_second + 1))
76
+ }
77
+
78
+ crew() {
79
+ if [[ "${1:-}" != "run" || "${2:-}" != "--watch" ]]; then
80
+ printf '%s\n' 'demo supports: crew run --watch'
81
+ return 2
82
+ fi
83
+
84
+ local pane_one
85
+
86
+ demo_log "${c_dim}Linear viewer ·${c_reset} Rocky Warren"
87
+ sleep 0.5
88
+ demo_log "${c_dim}Slots${c_reset} 0/3 ${c_dim}· dispatching${c_reset} ${c_amber}ENG-184${c_reset}${c_dim},${c_reset} ${c_blue}ENG-217${c_reset}"
89
+ sleep 0.6
90
+
91
+ demo_log "${c_dim}Worktree${c_reset} web-ENG-184 ${c_dim}→${c_reset} groundcrew/ENG-184"
92
+ sleep 0.5
93
+ pane_one="$(
94
+ tmux split-window -d -h -p 42 -P -F '#{pane_id}' -- \
95
+ "${demo_agent_script} ENG-184 codex 'Add Jira ticket source docs' web-ENG-184"
96
+ )"
97
+ sleep 0.7
98
+ demo_log "${c_green}✓${c_reset} ${c_amber}ENG-184${c_reset} launched ${c_dim}·${c_reset} ${c_amber}codex${c_reset}"
99
+ sleep 0.7
100
+
101
+ demo_log "${c_dim}Worktree${c_reset} api-ENG-217 ${c_dim}→${c_reset} groundcrew/ENG-217"
102
+ sleep 0.5
103
+ tmux split-window -d -v -p 50 -t "${pane_one}" -P -F '#{pane_id}' -- \
104
+ "${demo_agent_script} ENG-217 claude 'Fix flaky status output' api-ENG-217" >/dev/null
105
+ sleep 0.7
106
+ demo_log "${c_green}✓${c_reset} ${c_blue}ENG-217${c_reset} launched ${c_dim}·${c_reset} ${c_blue}claude${c_reset}"
107
+ sleep 0.7
108
+
109
+ demo_log "${c_dim}Queue clear · next poll in 60s${c_reset}"
110
+ sleep 8
111
+ }
package/static/demo.gif CHANGED
Binary file
@@ -0,0 +1,42 @@
1
+ # cspell:ignore noprofile norc ttyd
2
+
3
+ Output static/demo.gif
4
+ Require bash
5
+ Require ffmpeg
6
+ Require tmux
7
+ Require ttyd
8
+
9
+ Set Shell "bash"
10
+ Set Width 1320
11
+ Set Height 560
12
+ Set FontSize 16
13
+ Set FontFamily "Menlo"
14
+ Set Padding 16
15
+ Set Margin 40
16
+ Set MarginFill "#0a0a0b"
17
+ Set WindowBar Colorful
18
+ Set BorderRadius 12
19
+ Set TypingSpeed 35ms
20
+ Set CursorBlink false
21
+ Set Theme '{ "background": "#18181b", "foreground": "#e4e4e7", "cursor": "#77d94e", "selection": "#3f3f46", "black": "#27272a", "red": "#fb7185", "green": "#77d94e", "yellow": "#fbbf24", "blue": "#60a5fa", "magenta": "#c084fc", "cyan": "#34d399", "white": "#e4e4e7", "brightBlack": "#52525b", "brightRed": "#fda4af", "brightGreen": "#a3e635", "brightYellow": "#fcd34d", "brightBlue": "#93c5fd", "brightMagenta": "#d8b4fe", "brightCyan": "#6ee7b7", "brightWhite": "#fafafa" }'
22
+
23
+ Hide
24
+ Type "tmux -f /dev/null -L groundcrew-demo kill-session -t groundcrew 2>/dev/null || true"
25
+ Enter
26
+ Type "tmux -f /dev/null -L groundcrew-demo new-session -s groundcrew -- /bin/bash --noprofile --norc"
27
+ Enter
28
+ Type "source static/demo-fixture.sh"
29
+ Enter
30
+ Type "clear"
31
+ Enter
32
+ Show
33
+
34
+ Sleep 1.0
35
+ Type "crew run --watch"
36
+ Sleep 0.4
37
+ Enter
38
+ Sleep 9
39
+
40
+ Hide
41
+ Type "tmux -f /dev/null -L groundcrew-demo kill-session -t groundcrew"
42
+ Enter
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # cspell:ignore ttyd
3
+ set -euo pipefail
4
+
5
+ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
6
+ repo_dir="$(cd -- "${script_dir}/.." && pwd)"
7
+ cd "${repo_dir}"
8
+
9
+ for required_command in vhs ttyd tmux ffmpeg; do
10
+ if ! command -v "${required_command}" >/dev/null 2>&1; then
11
+ printf 'Missing required command: %s\n' "${required_command}" >&2
12
+ exit 127
13
+ fi
14
+ done
15
+
16
+ tmux -f /dev/null -L groundcrew-demo kill-session -t groundcrew 2>/dev/null || true
17
+
18
+ log_file="$(mktemp)"
19
+ trap 'rm -f "${log_file}"' EXIT
20
+
21
+ for attempt in 1 2 3; do
22
+ rm -f "${log_file}"
23
+
24
+ if VHS_NO_SANDBOX="${VHS_NO_SANDBOX:-1}" vhs static/demo.tape 2>&1 | tee "${log_file}"; then
25
+ exit 0
26
+ fi
27
+
28
+ status="${PIPESTATUS[0]}"
29
+ tmux -f /dev/null -L groundcrew-demo kill-session -t groundcrew 2>/dev/null || true
30
+
31
+ if ! grep -Eq 'could not open ttyd|ERR_CONNECTION_REFUSED' "${log_file}"; then
32
+ exit "${status}"
33
+ fi
34
+
35
+ if [[ "${attempt}" -eq 3 ]]; then
36
+ exit "${status}"
37
+ fi
38
+
39
+ sleep "${attempt}"
40
+ done