@clipboard-health/groundcrew 3.1.9 → 3.2.1

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.
Files changed (42) hide show
  1. package/README.md +10 -6
  2. package/crew.config.example.ts +19 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +12 -0
  5. package/dist/commands/dispatcher.d.ts.map +1 -1
  6. package/dist/commands/dispatcher.js +10 -10
  7. package/dist/commands/init.d.ts +26 -0
  8. package/dist/commands/init.d.ts.map +1 -0
  9. package/dist/commands/init.js +82 -0
  10. package/dist/commands/sandbox/auth.d.ts +3 -0
  11. package/dist/commands/sandbox/auth.d.ts.map +1 -0
  12. package/dist/commands/sandbox/auth.js +227 -0
  13. package/dist/commands/sandbox/index.d.ts +2 -0
  14. package/dist/commands/sandbox/index.d.ts.map +1 -0
  15. package/dist/commands/sandbox/index.js +47 -0
  16. package/dist/commands/sandbox/inspect.d.ts +2 -0
  17. package/dist/commands/sandbox/inspect.d.ts.map +1 -0
  18. package/dist/commands/sandbox/inspect.js +18 -0
  19. package/dist/commands/sandbox/lifecycle.d.ts +7 -0
  20. package/dist/commands/sandbox/lifecycle.d.ts.map +1 -0
  21. package/dist/commands/sandbox/lifecycle.js +68 -0
  22. package/dist/commands/sandbox/model.d.ts +10 -0
  23. package/dist/commands/sandbox/model.d.ts.map +1 -0
  24. package/dist/commands/sandbox/model.js +37 -0
  25. package/dist/commands/sandbox/picker.d.ts +20 -0
  26. package/dist/commands/sandbox/picker.d.ts.map +1 -0
  27. package/dist/commands/sandbox/picker.js +23 -0
  28. package/dist/lib/agentLaunch.d.ts.map +1 -1
  29. package/dist/lib/agentLaunch.js +1 -0
  30. package/dist/lib/config.d.ts +70 -0
  31. package/dist/lib/config.d.ts.map +1 -1
  32. package/dist/lib/config.js +79 -13
  33. package/dist/lib/dockerSandbox.d.ts +12 -8
  34. package/dist/lib/dockerSandbox.d.ts.map +1 -1
  35. package/dist/lib/dockerSandbox.js +33 -22
  36. package/dist/lib/sandboxGitDefaults.d.ts +10 -0
  37. package/dist/lib/sandboxGitDefaults.d.ts.map +1 -0
  38. package/dist/lib/sandboxGitDefaults.js +31 -0
  39. package/dist/lib/xdg.d.ts +3 -0
  40. package/dist/lib/xdg.d.ts.map +1 -0
  41. package/dist/lib/xdg.js +19 -0
  42. package/package.json +12 -11
@@ -0,0 +1,10 @@
1
+ import type { ResolvedConfig, SandboxDefinition } from "../../lib/config.ts";
2
+ export interface SandboxModel {
3
+ modelName: string;
4
+ sandbox: SandboxDefinition;
5
+ sandboxName: string;
6
+ }
7
+ export declare function sandboxModels(config: ResolvedConfig): SandboxModel[];
8
+ export declare function resolveModel(config: ResolvedConfig, modelName: string): SandboxModel;
9
+ export declare function requireOnePositional(argv: string[], usage: string): string;
10
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG7E,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,EAAE,CAcpE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAapF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM1E"}
@@ -0,0 +1,37 @@
1
+ import { sandboxNameFor } from "../../lib/dockerSandbox.js";
2
+ export function sandboxModels(config) {
3
+ const models = [];
4
+ for (const [modelName, definition] of Object.entries(config.models.definitions)) {
5
+ const { sandbox } = definition;
6
+ if (sandbox === undefined) {
7
+ continue;
8
+ }
9
+ models.push({
10
+ modelName,
11
+ sandbox,
12
+ sandboxName: sandboxNameFor({ agent: sandbox.agent }),
13
+ });
14
+ }
15
+ return models;
16
+ }
17
+ export function resolveModel(config, modelName) {
18
+ const definition = config.models.definitions[modelName];
19
+ if (definition === undefined) {
20
+ throw new Error(`crew sandbox: unknown model '${modelName}'`);
21
+ }
22
+ if (definition.sandbox === undefined) {
23
+ throw new Error(`crew sandbox: model '${modelName}' has no sandbox config`);
24
+ }
25
+ return {
26
+ modelName,
27
+ sandbox: definition.sandbox,
28
+ sandboxName: sandboxNameFor({ agent: definition.sandbox.agent }),
29
+ };
30
+ }
31
+ export function requireOnePositional(argv, usage) {
32
+ const [first, ...rest] = argv;
33
+ if (first === undefined || rest.length > 0) {
34
+ throw new Error(usage);
35
+ }
36
+ return first;
37
+ }
@@ -0,0 +1,20 @@
1
+ export interface ToolChoice {
2
+ /** Recipe key (e.g. "claude", "github"). Returned in the selection. */
3
+ key: string;
4
+ /** Human-friendly label shown in the prompt. */
5
+ label: string;
6
+ /** Auth status decoration: ✓ when authenticated, ○ otherwise. */
7
+ authenticated: boolean;
8
+ }
9
+ /**
10
+ * Show an interactive checkbox picker so the engineer chooses which
11
+ * tools to authenticate. Items marked `authenticated` start unchecked
12
+ * (no need to re-auth); unauthed items start checked (default action
13
+ * is "auth what's missing"). The returned array is the list of `key`
14
+ * values that the engineer left checked when they confirmed.
15
+ *
16
+ * Extracted to its own module so tests can vi.mock it and skip stdin
17
+ * interaction; the real implementation pulls @inquirer/checkbox.
18
+ */
19
+ export declare function pickTools(choices: readonly ToolChoice[]): Promise<readonly string[]>;
20
+ //# sourceMappingURL=picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/picker.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAW1F"}
@@ -0,0 +1,23 @@
1
+ import checkbox from "@inquirer/checkbox";
2
+ /**
3
+ * Show an interactive checkbox picker so the engineer chooses which
4
+ * tools to authenticate. Items marked `authenticated` start unchecked
5
+ * (no need to re-auth); unauthed items start checked (default action
6
+ * is "auth what's missing"). The returned array is the list of `key`
7
+ * values that the engineer left checked when they confirmed.
8
+ *
9
+ * Extracted to its own module so tests can vi.mock it and skip stdin
10
+ * interaction; the real implementation pulls @inquirer/checkbox.
11
+ */
12
+ export async function pickTools(choices) {
13
+ const selected = await checkbox({
14
+ message: "Select tools to authenticate (space to toggle, enter to confirm):",
15
+ choices: choices.map((choice) => ({
16
+ name: `${choice.authenticated ? "✓" : "○"} ${choice.label}`,
17
+ value: choice.key,
18
+ checked: !choice.authenticated,
19
+ })),
20
+ pageSize: Math.max(choices.length, 1),
21
+ });
22
+ return selected;
23
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOhF,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6B/B;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhB;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
1
+ {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOhF,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6B/B;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
@@ -37,6 +37,7 @@ export async function ensureAgentSandbox(input) {
37
37
  sandboxName: input.sandboxName,
38
38
  sandbox: input.definition.sandbox,
39
39
  mountPath: resolve(input.config.workspace.projectDir),
40
+ gitDefaults: input.config.sandbox.gitDefaults,
40
41
  }, input.signal);
41
42
  }
42
43
  }
@@ -58,6 +58,42 @@ export interface SandboxDefinition {
58
58
  */
59
59
  setupCommand?: string;
60
60
  }
61
+ /**
62
+ * Recipe used by `crew sandbox auth <model>` to drive an interactive
63
+ * login flow inside a sbx sandbox and then verify it. The flow is
64
+ * picker-driven — no positional `<tool>` argument; the picker lists
65
+ * every recipe visible to the current sandbox.
66
+ *
67
+ * `binary` defaults to the recipe key (typically the agent or CLI name).
68
+ * `authenticatedPattern` matches against combined stdout+stderr from
69
+ * `statusArgs` — exit code alone isn't reliable because some CLIs
70
+ * report "not logged in" while still exiting 0.
71
+ * `kind` controls visibility in the interactive picker: `"agent"`
72
+ * recipes are scoped to a specific sbx agent and only appear when you
73
+ * `auth` against that agent's sandbox; `"tool"` recipes (default)
74
+ * appear in every sandbox's picker because they're cross-cutting
75
+ * (github, npm, gcloud, …). Defaults to `"tool"` when omitted.
76
+ *
77
+ * Ship-side recipes for `claude`, `codex`, and `cursor` live in
78
+ * `src/commands/sandbox/auth.ts`; users register additional tools
79
+ * under `sandbox.authRecipes` in their config.
80
+ */
81
+ export interface AuthRecipe {
82
+ displayName: string;
83
+ binary?: string;
84
+ loginArgs: readonly string[];
85
+ statusArgs: readonly string[];
86
+ authenticatedPattern: RegExp;
87
+ kind?: "agent" | "tool";
88
+ /**
89
+ * Environment variables passed to `sbx exec` for both the login and
90
+ * status calls. Use this for CLIs whose default flow assumes a
91
+ * browser or other host-only feature — e.g. cursor-agent wants
92
+ * `NO_OPEN_BROWSER=1` to print a device code instead of trying to
93
+ * launch a browser inside the sandbox.
94
+ */
95
+ env?: Record<string, string>;
96
+ }
61
97
  export interface ModelDefinition {
62
98
  /**
63
99
  * Shell command launched for the model. Wrapped with Safehouse/clearance
@@ -192,6 +228,32 @@ export interface Config {
192
228
  local?: {
193
229
  runner?: LocalRunnerSetting;
194
230
  };
231
+ /**
232
+ * Sandbox-wide settings. `authRecipes` lets users register additional
233
+ * tools (github, npm, gcloud, …) for `crew sandbox auth <model>` to
234
+ * authenticate inside the sandbox. The auth flow is picker-driven —
235
+ * registered recipes show up in the picker alongside the shipped ones,
236
+ * and a user recipe under the same key (e.g. "claude") overrides the
237
+ * shipped one.
238
+ */
239
+ sandbox?: {
240
+ authRecipes?: Record<string, AuthRecipe>;
241
+ /**
242
+ * When true (default), every `crew sandbox ensure` / `auth` run applies
243
+ * a small set of git defaults inside the sandbox so robot commits push
244
+ * over `gh`-managed HTTPS regardless of how the user cloned the repo:
245
+ *
246
+ * - disable GPG signing for commits and tags
247
+ * - rewrite `git@github.com:` and `ssh://git@github.com/` URLs to
248
+ * `https://github.com/` so push uses gh's credential helper
249
+ * - after a successful `github` auth recipe login, run
250
+ * `gh auth setup-git` inside the sandbox
251
+ *
252
+ * Set `false` to skip both the git-config block and the post-login
253
+ * `gh auth setup-git` step.
254
+ */
255
+ gitDefaults?: boolean;
256
+ };
195
257
  logging?: {
196
258
  /**
197
259
  * Append-mode log file destination. `log()` and `logEvent()` tee here
@@ -261,6 +323,14 @@ export interface ResolvedConfig {
261
323
  local: {
262
324
  runner: LocalRunnerSetting;
263
325
  };
326
+ /**
327
+ * Sandbox-wide settings. Always present after defaults; `authRecipes`
328
+ * is `{}` when the user provides none.
329
+ */
330
+ sandbox: {
331
+ authRecipes: Record<string, AuthRecipe>;
332
+ gitDefaults: boolean;
333
+ };
264
334
  logging: {
265
335
  file: string;
266
336
  };
@@ -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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"}
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { cosmiconfig } from "cosmiconfig";
7
7
  import { log, readEnvironmentVariable, setLogFile } from "./util.js";
8
+ import { xdgConfigPath, xdgStatePath } from "./xdg.js";
8
9
  export { BUILD_SECRET_NAMES } from "./buildSecrets.js";
9
10
  /**
10
11
  * Reserved model name. A ticket labeled `agent-any` resolves at runtime
@@ -83,19 +84,6 @@ const ALLOWED_PROMPT_PLACEHOLDERS = new Set([
83
84
  const PROMPT_PLACEHOLDER_RE = /{{[^{}]*}}/g;
84
85
  const PERCENT_MIN_EXCLUSIVE = 0;
85
86
  const PERCENT_MAX = 100;
86
- function xdgBase(envName, fallbackSegments) {
87
- const override = readEnvironmentVariable(envName);
88
- if (override !== undefined && override.length > 0) {
89
- return override;
90
- }
91
- return resolve(homedir(), ...fallbackSegments);
92
- }
93
- function xdgConfigPath(...segments) {
94
- return resolve(xdgBase("XDG_CONFIG_HOME", [".config"]), ...segments);
95
- }
96
- function xdgStatePath(...segments) {
97
- return resolve(xdgBase("XDG_STATE_HOME", [".local", "state"]), ...segments);
98
- }
99
87
  function defaultLogFile() {
100
88
  return xdgStatePath("groundcrew", "groundcrew.log");
101
89
  }
@@ -144,6 +132,15 @@ function normalizeOptionalString(value, path) {
144
132
  }
145
133
  return value.trim();
146
134
  }
135
+ function normalizeOptionalBoolean(value, path) {
136
+ if (value === undefined) {
137
+ return undefined;
138
+ }
139
+ if (typeof value !== "boolean") {
140
+ fail(`${path} must be a boolean`);
141
+ }
142
+ return value;
143
+ }
147
144
  function normalizeOptionalStringArray(value, path) {
148
145
  if (value === undefined) {
149
146
  return undefined;
@@ -337,6 +334,11 @@ function requireObject(value, path) {
337
334
  fail(`${path} must be an object (got ${JSON.stringify(value)})`);
338
335
  }
339
336
  }
337
+ function requireOptionalObject(value, path) {
338
+ if (value !== undefined && !isPlainObject(value)) {
339
+ fail(`${path} must be an object`);
340
+ }
341
+ }
340
342
  function normalizeProject(value, index) {
341
343
  const path = `linear.projects[${index}]`;
342
344
  if (!isPlainObject(value)) {
@@ -441,6 +443,65 @@ function normalizeProjects(linear) {
441
443
  });
442
444
  return resolved;
443
445
  }
446
+ function normalizeAuthRecipes(value, path) {
447
+ if (value === undefined) {
448
+ return {};
449
+ }
450
+ if (!isPlainObject(value)) {
451
+ fail(`${path} must be an object`);
452
+ }
453
+ const recipes = {};
454
+ for (const [key, raw] of Object.entries(value)) {
455
+ const recipePath = `${path}.${key}`;
456
+ if (!isPlainObject(raw)) {
457
+ fail(`${recipePath} must be an object`);
458
+ }
459
+ const { displayName, binary, loginArgs, statusArgs, authenticatedPattern, kind, env } = raw;
460
+ requireString(displayName, `${recipePath}.displayName`);
461
+ const loginArray = normalizeOptionalStringArray(loginArgs, `${recipePath}.loginArgs`);
462
+ const statusArray = normalizeOptionalStringArray(statusArgs, `${recipePath}.statusArgs`);
463
+ if (loginArray === undefined) {
464
+ fail(`${recipePath}.loginArgs is required`);
465
+ }
466
+ if (statusArray === undefined) {
467
+ fail(`${recipePath}.statusArgs is required`);
468
+ }
469
+ if (!(authenticatedPattern instanceof RegExp)) {
470
+ fail(`${recipePath}.authenticatedPattern must be a RegExp`);
471
+ }
472
+ const recipe = {
473
+ displayName,
474
+ loginArgs: loginArray,
475
+ statusArgs: statusArray,
476
+ authenticatedPattern,
477
+ };
478
+ const binaryString = normalizeOptionalString(binary, `${recipePath}.binary`);
479
+ if (binaryString !== undefined) {
480
+ recipe.binary = binaryString;
481
+ }
482
+ if (kind !== undefined) {
483
+ if (kind !== "agent" && kind !== "tool") {
484
+ fail(`${recipePath}.kind must be "agent" or "tool"`);
485
+ }
486
+ recipe.kind = kind;
487
+ }
488
+ if (env !== undefined) {
489
+ if (!isPlainObject(env)) {
490
+ fail(`${recipePath}.env must be an object`);
491
+ }
492
+ const normalizedEnv = {};
493
+ for (const [envKey, envValue] of Object.entries(env)) {
494
+ if (typeof envValue !== "string") {
495
+ fail(`${recipePath}.env.${envKey} must be a string`);
496
+ }
497
+ normalizedEnv[envKey] = envValue;
498
+ }
499
+ recipe.env = normalizedEnv;
500
+ }
501
+ recipes[key] = recipe;
502
+ }
503
+ return recipes;
504
+ }
444
505
  function applyDefaults(user) {
445
506
  // Guard the top-level shape before reading nested fields, so a
446
507
  // malformed runtime config produces a `groundcrew config: ...` error
@@ -455,6 +516,7 @@ function applyDefaults(user) {
455
516
  if (Object.hasOwn(user, "remote")) {
456
517
  fail("remote is no longer supported: groundcrew runs locally via safehouse/sdx/none; remove the remote block from your config");
457
518
  }
519
+ requireOptionalObject(user.sandbox, "sandbox");
458
520
  const userLocal = user.local;
459
521
  if (userLocal !== undefined && !isPlainObject(userLocal)) {
460
522
  fail("local must be an object");
@@ -482,6 +544,10 @@ function applyDefaults(user) {
482
544
  local: {
483
545
  runner: normalizeLocalRunner(userLocal?.runner, "local.runner") ?? "auto",
484
546
  },
547
+ sandbox: {
548
+ authRecipes: normalizeAuthRecipes(user.sandbox?.authRecipes, "sandbox.authRecipes"),
549
+ gitDefaults: normalizeOptionalBoolean(user.sandbox?.gitDefaults, "sandbox.gitDefaults") ?? true,
550
+ },
485
551
  logging: {
486
552
  file: expandHome(normalizeOptionalString(user.logging?.file, "logging.file") ?? defaultLogFile()),
487
553
  },
@@ -25,15 +25,19 @@ interface EnsureSandboxArguments {
25
25
  * clone) are visible to `sbx exec -w <worktreeDir>` after creation.
26
26
  */
27
27
  mountPath: string;
28
+ /**
29
+ * When true, apply the standard git defaults inside the sandbox after
30
+ * it exists (idempotent, runs whether the sandbox was just created or
31
+ * already there). See `sandboxGitDefaults.ts` for what gets set.
32
+ */
33
+ gitDefaults: boolean;
34
+ /**
35
+ * Result of an earlier `sandboxExists` probe by the caller, used to
36
+ * skip the initial `sbx ls` here. Leave undefined to let this function
37
+ * probe on its own.
38
+ */
39
+ alreadyExists?: boolean;
28
40
  }
29
- /**
30
- * Idempotent guard: ensure a Docker Sandboxes container exists for the
31
- * given repository + model. Probes `sbx ls`; if `sandboxName` is missing,
32
- * calls `sbx create --name <name> [--template <t>] [--kit <k>]... <agent>
33
- * <mountPath>` to provision it. First-time agent auth still happens inside
34
- * the sandbox the first time `sbx exec` runs the agent — `create` only
35
- * provisions the container, it does not attach.
36
- */
37
41
  export declare function ensureSandbox(arguments_: EnsureSandboxArguments, signal?: AbortSignal): Promise<void>;
38
42
  export {};
39
43
  //# sourceMappingURL=dockerSandbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dockerSandbox.d.ts","sourceRoot":"","sources":["../../src/lib/dockerSandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMpE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/F;AAED,UAAU,sBAAsB;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,sBAAsB,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
1
+ {"version":3,"file":"dockerSandbox.d.ts","sourceRoot":"","sources":["../../src/lib/dockerSandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGrD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMpE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/F;AAED,UAAU,sBAAsB;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAsBD,wBAAsB,aAAa,CACjC,UAAU,EAAE,sBAAsB,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAuBf"}
@@ -1,4 +1,5 @@
1
1
  import { runCommandAsync } from "./commandRunner.js";
2
+ import { applyGitDefaults } from "./sandboxGitDefaults.js";
2
3
  /**
3
4
  * Derive a deterministic sbx sandbox name from the sbx agent so every
4
5
  * groundcrew model that targets the same agent reuses one sandbox across
@@ -29,30 +30,40 @@ export async function sandboxExists(sandboxName, signal) {
29
30
  * Idempotent guard: ensure a Docker Sandboxes container exists for the
30
31
  * given repository + model. Probes `sbx ls`; if `sandboxName` is missing,
31
32
  * calls `sbx create --name <name> [--template <t>] [--kit <k>]... <agent>
32
- * <mountPath>` to provision it. First-time agent auth still happens inside
33
- * the sandbox the first time `sbx exec` runs the agent — `create` only
34
- * provisions the container, it does not attach.
33
+ * <mountPath>` to provision it. Once the container exists (newly created
34
+ * or pre-existing), applies the standard git defaults when enabled.
35
+ * First-time agent auth still happens inside the sandbox the first time
36
+ * `sbx exec` runs the agent — `create` only provisions the container, it
37
+ * does not attach.
35
38
  */
36
- export async function ensureSandbox(arguments_, signal) {
37
- if (await sandboxExists(arguments_.sandboxName, signal)) {
38
- return;
39
- }
40
- const createArguments = ["create", "--name", arguments_.sandboxName];
41
- if (arguments_.sandbox.template !== undefined) {
42
- createArguments.push("--template", arguments_.sandbox.template);
43
- }
44
- for (const kit of arguments_.sandbox.kits ?? []) {
45
- createArguments.push("--kit", kit);
39
+ async function resolveExistence(arguments_, signal) {
40
+ if (arguments_.alreadyExists === undefined) {
41
+ return await sandboxExists(arguments_.sandboxName, signal);
46
42
  }
47
- createArguments.push(arguments_.sandbox.agent, arguments_.mountPath);
48
- const options = signal === undefined ? {} : { signal };
49
- try {
50
- await runCommandAsync("sbx", createArguments, options);
51
- }
52
- catch (error) {
53
- if (await sandboxExists(arguments_.sandboxName, signal)) {
54
- return;
43
+ return arguments_.alreadyExists;
44
+ }
45
+ export async function ensureSandbox(arguments_, signal) {
46
+ const existed = await resolveExistence(arguments_, signal);
47
+ if (!existed) {
48
+ const createArguments = ["create", "--name", arguments_.sandboxName];
49
+ if (arguments_.sandbox.template !== undefined) {
50
+ createArguments.push("--template", arguments_.sandbox.template);
51
+ }
52
+ for (const kit of arguments_.sandbox.kits ?? []) {
53
+ createArguments.push("--kit", kit);
54
+ }
55
+ createArguments.push(arguments_.sandbox.agent, arguments_.mountPath);
56
+ const options = signal === undefined ? {} : { signal };
57
+ try {
58
+ await runCommandAsync("sbx", createArguments, options);
55
59
  }
56
- throw error;
60
+ catch (error) {
61
+ if (!(await sandboxExists(arguments_.sandboxName, signal))) {
62
+ throw error;
63
+ }
64
+ }
65
+ }
66
+ if (arguments_.gitDefaults) {
67
+ await applyGitDefaults({ sandboxName: arguments_.sandboxName }, signal);
57
68
  }
58
69
  }
@@ -0,0 +1,10 @@
1
+ interface ApplyGitDefaultsArguments {
2
+ sandboxName: string;
3
+ }
4
+ /**
5
+ * Apply the standard git defaults inside `sandboxName`. Idempotent —
6
+ * safe to call on every `ensure`/`auth` run to repair drift.
7
+ */
8
+ export declare function applyGitDefaults(arguments_: ApplyGitDefaultsArguments, signal?: AbortSignal): Promise<void>;
9
+ export {};
10
+ //# sourceMappingURL=sandboxGitDefaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandboxGitDefaults.d.ts","sourceRoot":"","sources":["../../src/lib/sandboxGitDefaults.ts"],"names":[],"mappings":"AAyBA,UAAU,yBAAyB;IACjC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,yBAAyB,EACrC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAOf"}
@@ -0,0 +1,31 @@
1
+ import { runCommandAsync } from "./commandRunner.js";
2
+ /**
3
+ * Git defaults applied inside every sandbox when `sandbox.gitDefaults`
4
+ * is enabled (the default).
5
+ *
6
+ * - Disable GPG signing — robot commits inside the sandbox have no key
7
+ * and would otherwise fail or end up unsigned silently.
8
+ * - Rewrite GitHub SSH URLs to HTTPS so push/fetch go through the `gh`
9
+ * credential helper (wired by `gh auth setup-git` after a successful
10
+ * `crew sandbox auth` github login), regardless of how the user
11
+ * originally cloned the repo on the host.
12
+ *
13
+ * `url.<base>.insteadOf` is multi-valued in git; `--unset-all` before
14
+ * `--add` keeps the set identical across repeated runs instead of
15
+ * appending duplicates.
16
+ */
17
+ const GIT_DEFAULT_COMMANDS = [
18
+ "git config --global commit.gpgsign false",
19
+ "git config --global tag.gpgsign false",
20
+ '(git config --global --unset-all url."https://github.com/".insteadOf 2>/dev/null || true)',
21
+ 'git config --global --add url."https://github.com/".insteadOf "git@github.com:"',
22
+ 'git config --global --add url."https://github.com/".insteadOf "ssh://git@github.com/"',
23
+ ].join(" && ");
24
+ /**
25
+ * Apply the standard git defaults inside `sandboxName`. Idempotent —
26
+ * safe to call on every `ensure`/`auth` run to repair drift.
27
+ */
28
+ export async function applyGitDefaults(arguments_, signal) {
29
+ const options = signal === undefined ? {} : { signal };
30
+ await runCommandAsync("sbx", ["exec", arguments_.sandboxName, "sh", "-c", GIT_DEFAULT_COMMANDS], options);
31
+ }
@@ -0,0 +1,3 @@
1
+ export declare function xdgConfigPath(...segments: string[]): string;
2
+ export declare function xdgStatePath(...segments: string[]): string;
3
+ //# sourceMappingURL=xdg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xdg.d.ts","sourceRoot":"","sources":["../../src/lib/xdg.ts"],"names":[],"mappings":"AAgBA,wBAAgB,aAAa,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAE3D;AAED,wBAAgB,YAAY,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAE1D"}
@@ -0,0 +1,19 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { readEnvironmentVariable } from "./util.js";
4
+ // Per the XDG Base Directory spec, relative override paths are invalid and
5
+ // must be ignored — without this guard, a relative override would anchor to
6
+ // the cwd via `resolve` instead of falling back to $HOME.
7
+ function xdgBase(envName, fallbackSegments) {
8
+ const override = readEnvironmentVariable(envName);
9
+ if (override !== undefined && override.length > 0 && isAbsolute(override)) {
10
+ return override;
11
+ }
12
+ return resolve(homedir(), ...fallbackSegments);
13
+ }
14
+ export function xdgConfigPath(...segments) {
15
+ return resolve(xdgBase("XDG_CONFIG_HOME", [".config"]), ...segments);
16
+ }
17
+ export function xdgStatePath(...segments) {
18
+ return resolve(xdgBase("XDG_STATE_HOME", [".local", "state"]), ...segments);
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "3.1.9",
3
+ "version": "3.2.1",
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",
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@clipboard-health/clearance": "1.0.8",
71
+ "@inquirer/checkbox": "5.1.5",
71
72
  "@linear/sdk": "85.0.0",
72
73
  "cosmiconfig": "9.0.1",
73
74
  "tslib": "2.8.1",
@@ -76,26 +77,26 @@
76
77
  "devDependencies": {
77
78
  "@clipboard-health/ai-rules": "2.18.7",
78
79
  "@clipboard-health/oxlint-config": "1.9.4",
79
- "@nx/js": "22.7.1",
80
+ "@nx/js": "22.7.2",
80
81
  "@tsconfig/node24": "24.0.4",
81
82
  "@tsconfig/strictest": "2.0.8",
82
- "@types/node": "24.12.2",
83
+ "@types/node": "24.12.4",
83
84
  "@typescript/native-preview": "7.0.0-dev.20260522.1",
84
- "@vitest/coverage-v8": "4.1.5",
85
+ "@vitest/coverage-v8": "4.1.6",
85
86
  "cspell": "10.0.0",
86
87
  "dependency-cruiser": "17.4.0",
87
88
  "husky": "9.1.7",
88
- "jscpd": "4.0.9",
89
- "knip": "6.9.0",
89
+ "jscpd": "4.2.3",
90
+ "knip": "6.14.1",
90
91
  "lint-staged": "17.0.5",
91
92
  "markdownlint-cli2": "0.22.1",
92
- "nx": "22.7.1",
93
+ "nx": "22.7.2",
93
94
  "oxfmt": "0.50.0",
94
- "oxlint": "1.64.0",
95
- "oxlint-tsgolint": "0.22.1",
96
- "syncpack": "15.0.0",
95
+ "oxlint": "1.65.0",
96
+ "oxlint-tsgolint": "0.23.0",
97
+ "syncpack": "15.2.0",
97
98
  "vite": "8.0.14",
98
- "vitest": "4.1.5"
99
+ "vitest": "4.1.6"
99
100
  },
100
101
  "engines": {
101
102
  "node": "24.14.1",