@clipboard-health/groundcrew 4.24.3 → 4.25.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 +1 -1
- package/crew.config.example.ts +4 -3
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +4 -1
- package/dist/lib/host.d.ts +2 -0
- package/dist/lib/host.d.ts.map +1 -1
- package/dist/lib/host.js +3 -1
- package/dist/lib/util.d.ts +8 -0
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +25 -1
- package/dist/lib/workspaceAdapter.d.ts +1 -1
- package/dist/lib/workspaceAdapter.d.ts.map +1 -1
- package/dist/lib/workspaces.d.ts +4 -4
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +21 -8
- package/dist/lib/zellijAdapter.d.ts +26 -0
- package/dist/lib/zellijAdapter.d.ts.map +1 -0
- package/dist/lib/zellijAdapter.js +317 -0
- package/docs/commands.md +1 -1
- package/docs/configuration.md +2 -2
- package/docs/troubleshooting.md +6 -0
- package/package.json +1 -1
- package/static/zellij.png +0 -0
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Groundcrew watches assigned tasks, creates isolated worktrees, launches agent CL
|
|
|
37
37
|
|
|
38
38
|
- **Node >= 24:** [nvm](https://github.com/nvm-sh/nvm): `nvm install 24`.
|
|
39
39
|
- **git:** e.g., `brew install git`, `apt install git`.
|
|
40
|
-
- **A terminal multiplexer:** [tmux](https://github.com/tmux/tmux/wiki/Installing) (cross-platform)
|
|
40
|
+
- **A terminal multiplexer:** [tmux](https://github.com/tmux/tmux/wiki/Installing) (cross-platform), [cmux](https://cmux.com/) (macOS), or [zellij](https://zellij.dev/).
|
|
41
41
|
- **An agent CLI:** [Claude Code](https://code.claude.com/docs/en/quickstart) and/or [Codex](https://developers.openai.com/codex/quickstart?setup=cli).
|
|
42
42
|
- **A sandbox runner:** [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) (cross-platform) or [Safehouse](https://agent-safehouse.dev/) on macOS. Skip only with `--runner none`.
|
|
43
43
|
|
package/crew.config.example.ts
CHANGED
|
@@ -123,9 +123,10 @@ export default {
|
|
|
123
123
|
// },
|
|
124
124
|
//
|
|
125
125
|
// // Terminal session manager. "auto" picks cmux when on PATH, else tmux.
|
|
126
|
-
// // Set explicitly to "cmux" or "
|
|
127
|
-
// // backend is missing. tmux windows live in a
|
|
128
|
-
// // session and lose status-pill painting (
|
|
126
|
+
// // Set explicitly to "cmux", "tmux", or "zellij" to fail loudly when the
|
|
127
|
+
// // chosen backend is missing. tmux windows / zellij tabs live in a
|
|
128
|
+
// // dedicated `groundcrew` session and lose status-pill painting (a
|
|
129
|
+
// // cmux-only feature).
|
|
129
130
|
// workspaceKind: "auto",
|
|
130
131
|
//
|
|
131
132
|
// logging: {
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -25,8 +25,9 @@ export declare const AGENT_ANY_MODEL = "any";
|
|
|
25
25
|
* - `auto`: pick the first available — cmux when installed, else tmux.
|
|
26
26
|
* - `cmux`: require the cmux binary; fail loudly if missing.
|
|
27
27
|
* - `tmux`: require the tmux binary; fail loudly if missing.
|
|
28
|
+
* - `zellij`: require the zellij binary; fail loudly if missing.
|
|
28
29
|
*/
|
|
29
|
-
export type WorkspaceKindSetting = "auto" | "cmux" | "tmux";
|
|
30
|
+
export type WorkspaceKindSetting = "auto" | "cmux" | "tmux" | "zellij";
|
|
30
31
|
export declare const WORKSPACE_KIND_SETTINGS: readonly WorkspaceKindSetting[];
|
|
31
32
|
/**
|
|
32
33
|
* Concrete local isolation backend selected for a launch. `safehouse` is
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAO1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAE3F,MAAM,WAAW,YAAY;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -19,6 +19,7 @@ export const WORKSPACE_KIND_SETTINGS = [
|
|
|
19
19
|
"auto",
|
|
20
20
|
"cmux",
|
|
21
21
|
"tmux",
|
|
22
|
+
"zellij",
|
|
22
23
|
];
|
|
23
24
|
export const LOCAL_RUNNER_SETTINGS = [
|
|
24
25
|
"auto",
|
|
@@ -90,9 +91,11 @@ const MODEL_DEFINITIONS_MIGRATION_MESSAGE = [
|
|
|
90
91
|
const DEFAULT_PROMPT_INITIAL = [
|
|
91
92
|
"You are working on task {{task}} ({{title}}) in the {{worktree}} worktree subdirectory.",
|
|
92
93
|
"",
|
|
93
|
-
"Task description
|
|
94
|
+
"## Task description",
|
|
94
95
|
"",
|
|
96
|
+
"<task_description>",
|
|
95
97
|
"{{description}}",
|
|
98
|
+
"</task_description>",
|
|
96
99
|
"",
|
|
97
100
|
"## Operating mode",
|
|
98
101
|
"",
|
package/dist/lib/host.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface HostCapabilities {
|
|
|
12
12
|
hasCmux: boolean;
|
|
13
13
|
/** True when the `tmux` binary is on PATH. */
|
|
14
14
|
hasTmux: boolean;
|
|
15
|
+
/** True when the `zellij` binary is on PATH. */
|
|
16
|
+
hasZellij: boolean;
|
|
15
17
|
/** True when the `bubblewrap` binary is on PATH (Linux srt dependency). */
|
|
16
18
|
hasBubblewrap: boolean;
|
|
17
19
|
/** True when the `socat` binary is on PATH (Linux srt dependency). */
|
package/dist/lib/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/lib/host.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,YAAY,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,MAAM,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,aAAa,EAAE,OAAO,CAAC;IACvB,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAC;IACpB,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,oBAAoB,EAAE,OAAO,CAAC;IAC9B;;;;;;OAMG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAc1F;AAED,wBAAsB,sBAAsB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/lib/host.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,YAAY,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,MAAM,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,aAAa,EAAE,OAAO,CAAC;IACvB,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAC;IACpB,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,oBAAoB,EAAE,OAAO,CAAC;IAC9B;;;;;;OAMG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAc1F;AAED,wBAAsB,sBAAsB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4B5F"}
|
package/dist/lib/host.js
CHANGED
|
@@ -28,11 +28,12 @@ export async function which(cmd, signal) {
|
|
|
28
28
|
export async function detectHostCapabilities(signal) {
|
|
29
29
|
const isMacOS = process.platform === "darwin";
|
|
30
30
|
const isLinux = process.platform === "linux";
|
|
31
|
-
const [safehouse, sbx, cmux, tmux, bubblewrap, socat, ripgrep] = await Promise.all([
|
|
31
|
+
const [safehouse, sbx, cmux, tmux, zellij, bubblewrap, socat, ripgrep] = await Promise.all([
|
|
32
32
|
which("safehouse", signal),
|
|
33
33
|
which("sbx", signal),
|
|
34
34
|
which("cmux", signal),
|
|
35
35
|
which("tmux", signal),
|
|
36
|
+
which("zellij", signal),
|
|
36
37
|
which("bwrap", signal),
|
|
37
38
|
which("socat", signal),
|
|
38
39
|
which("rg", signal),
|
|
@@ -42,6 +43,7 @@ export async function detectHostCapabilities(signal) {
|
|
|
42
43
|
hasSbx: sbx !== undefined,
|
|
43
44
|
hasCmux: cmux !== undefined,
|
|
44
45
|
hasTmux: tmux !== undefined,
|
|
46
|
+
hasZellij: zellij !== undefined,
|
|
45
47
|
hasBubblewrap: bubblewrap !== undefined,
|
|
46
48
|
hasSocat: socat !== undefined,
|
|
47
49
|
hasRipgrep: ripgrep !== undefined,
|
package/dist/lib/util.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export declare function failMark(): string;
|
|
|
8
8
|
export declare function styleWarning(text: string): string;
|
|
9
9
|
export declare function styleDim(text: string): string;
|
|
10
10
|
export declare function setLogFile(filePath: string | undefined): void;
|
|
11
|
+
/** The resolved log file path, or undefined before `setLogFile` runs. */
|
|
12
|
+
export declare function getLogFile(): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Byte offset in the log file where the current run's output begins (the
|
|
15
|
+
* file size when `setLogFile` ran). `tail -c +N` from this offset shows only
|
|
16
|
+
* this run.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getLogRunStartByte(): number;
|
|
11
19
|
export declare function withLogOutputSuppressed<T>(operation: () => Promise<T>): Promise<T>;
|
|
12
20
|
/** Important tier: always on the console (dimmed timestamp) and the log file. */
|
|
13
21
|
export declare function log(message: string): void;
|
package/dist/lib/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAIA,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAQD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAE/C;AAED,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAWD,wBAAgB,MAAM,IAAI,MAAM,CAE/B;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAIA,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAQD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAE/C;AAED,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAWD,wBAAgB,MAAM,IAAI,MAAM,CAE/B;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AASD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAY7D;AAED,yEAAyE;AACzE,wBAAgB,UAAU,IAAI,MAAM,GAAG,SAAS,CAE/C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOxF;AA8BD,iFAAiF;AACjF,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAOzC;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAS3C;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAiBxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAMvF;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAcvF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
|
package/dist/lib/util.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, mkdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { styleText } from "node:util";
|
|
4
4
|
export async function sleep(ms, signal) {
|
|
@@ -61,9 +61,33 @@ export function styleDim(text) {
|
|
|
61
61
|
// so tests don't write to the host filesystem; the CLI arms it after
|
|
62
62
|
// loadConfig() resolves `logging.file`.
|
|
63
63
|
let logFilePath;
|
|
64
|
+
let logRunStartByte = 0;
|
|
64
65
|
let suppressedLogDepth = 0;
|
|
65
66
|
export function setLogFile(filePath) {
|
|
66
67
|
logFilePath = filePath;
|
|
68
|
+
// Snapshot the file size at process start so consumers can show only the
|
|
69
|
+
// current run's output (the log is append-mode and shared across runs).
|
|
70
|
+
logRunStartByte = 0;
|
|
71
|
+
if (filePath !== undefined) {
|
|
72
|
+
try {
|
|
73
|
+
logRunStartByte = statSync(filePath).size;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
logRunStartByte = 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The resolved log file path, or undefined before `setLogFile` runs. */
|
|
81
|
+
export function getLogFile() {
|
|
82
|
+
return logFilePath;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Byte offset in the log file where the current run's output begins (the
|
|
86
|
+
* file size when `setLogFile` ran). `tail -c +N` from this offset shows only
|
|
87
|
+
* this run.
|
|
88
|
+
*/
|
|
89
|
+
export function getLogRunStartByte() {
|
|
90
|
+
return logRunStartByte;
|
|
67
91
|
}
|
|
68
92
|
export async function withLogOutputSuppressed(operation) {
|
|
69
93
|
suppressedLogDepth += 1;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `workspaces.ts` resolves and fronts them. This is internal cleanup, not a
|
|
7
7
|
* plugin contract — nothing here is a published extension point.
|
|
8
8
|
*/
|
|
9
|
-
export type WorkspaceKind = "cmux" | "tmux";
|
|
9
|
+
export type WorkspaceKind = "cmux" | "tmux" | "zellij";
|
|
10
10
|
export interface Workspace {
|
|
11
11
|
/** Task id; the join key callers use. */
|
|
12
12
|
name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaceAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/workspaceAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"workspaceAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/workspaceAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,wBAAwB,GAChC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;;;;OAKG;IACH,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC,CAAC;IACjE,0DAA0D;IAC1D,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC7E;;;OAGG;IACH,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,mBAAmB,GAAG,SAAS,CAAC;CAC/D;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAE7D"}
|
package/dist/lib/workspaces.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace facade — opens/lists/closes the host-side terminal session
|
|
3
3
|
* that runs an agent for one task. `Workspace.name` is the task id;
|
|
4
|
-
* callers key on it.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* callers key on it. Backend implementations live in their own files behind
|
|
5
|
+
* the shared `Adapter` interface in `workspaceAdapter.ts`; this module
|
|
6
|
+
* resolves and lazy-loads the selected one, caches it per config, and exposes
|
|
7
|
+
* the `workspaces` API.
|
|
8
8
|
*/
|
|
9
9
|
import type { ResolvedConfig, WorkspaceKindSetting } from "./config.ts";
|
|
10
10
|
import { type HostCapabilities } from "./host.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AAsED,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED,iBAAe,sBAAsB,CACnC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAG1C;AAED,iBAAe,kBAAkB,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SACD,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC;IAIhC,SAAS;IACT,UAAU;CACX,CAAC"}
|
package/dist/lib/workspaces.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace facade — opens/lists/closes the host-side terminal session
|
|
3
3
|
* that runs an agent for one task. `Workspace.name` is the task id;
|
|
4
|
-
* callers key on it.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* callers key on it. Backend implementations live in their own files behind
|
|
5
|
+
* the shared `Adapter` interface in `workspaceAdapter.ts`; this module
|
|
6
|
+
* resolves and lazy-loads the selected one, caches it per config, and exposes
|
|
7
|
+
* the `workspaces` API.
|
|
8
8
|
*/
|
|
9
|
-
import { cmuxAdapter } from "./cmuxAdapter.js";
|
|
10
9
|
import { detectHostCapabilities } from "./host.js";
|
|
11
|
-
import { tmuxAdapter } from "./tmuxAdapter.js";
|
|
12
10
|
import { isSignalAborted, } from "./workspaceAdapter.js";
|
|
13
11
|
export function resolveWorkspaceKind(arguments_) {
|
|
14
12
|
const { config, host } = arguments_;
|
|
15
13
|
const requested = config.workspaceKind;
|
|
16
|
-
if (requested
|
|
14
|
+
if (requested !== "auto") {
|
|
17
15
|
failIfBinaryUnavailable(requested, host);
|
|
18
16
|
return { requested, resolved: requested, reason: `workspaceKind set to ${requested}` };
|
|
19
17
|
}
|
|
@@ -36,6 +34,21 @@ function resolveAuto(arguments_) {
|
|
|
36
34
|
const HOST_CAPABILITY_BY_KIND = {
|
|
37
35
|
cmux: "hasCmux",
|
|
38
36
|
tmux: "hasTmux",
|
|
37
|
+
zellij: "hasZellij",
|
|
38
|
+
};
|
|
39
|
+
const ADAPTER_LOADER_BY_KIND = {
|
|
40
|
+
cmux: async () => {
|
|
41
|
+
const { cmuxAdapter } = await import("./cmuxAdapter.js");
|
|
42
|
+
return cmuxAdapter;
|
|
43
|
+
},
|
|
44
|
+
tmux: async () => {
|
|
45
|
+
const { tmuxAdapter } = await import("./tmuxAdapter.js");
|
|
46
|
+
return tmuxAdapter;
|
|
47
|
+
},
|
|
48
|
+
zellij: async () => {
|
|
49
|
+
const { zellijAdapter } = await import("./zellijAdapter.js");
|
|
50
|
+
return zellijAdapter;
|
|
51
|
+
},
|
|
39
52
|
};
|
|
40
53
|
function failIfBinaryUnavailable(kind, host) {
|
|
41
54
|
if (!host[HOST_CAPABILITY_BY_KIND[kind]]) {
|
|
@@ -55,7 +68,7 @@ async function adapterFor(config, signal) {
|
|
|
55
68
|
config,
|
|
56
69
|
host: await detectHostCapabilities(signal),
|
|
57
70
|
});
|
|
58
|
-
const adapter = resolved
|
|
71
|
+
const adapter = await ADAPTER_LOADER_BY_KIND[resolved]();
|
|
59
72
|
adapterCache.set(config, adapter);
|
|
60
73
|
return adapter;
|
|
61
74
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zellij Workspace backend. Workspaces live as named tabs inside one
|
|
3
|
+
* dedicated `groundcrew` zellij session; the tab name is the ticket id and
|
|
4
|
+
* the first tab (`main`) is a placeholder that keeps the session alive. This
|
|
5
|
+
* is a Linux/WSL alternative to tmux: zellij is a stateful multiplexer, so it
|
|
6
|
+
* restores screen + terminal modes (mouse reporting, alt-screen) on every
|
|
7
|
+
* attach, and enables the mouse by default. Zellij can't paint status pills,
|
|
8
|
+
* so `open` silently drops `spec.status`.
|
|
9
|
+
*
|
|
10
|
+
* Zellij quirks shape this adapter:
|
|
11
|
+
* 1. Tab actions that target the *active* tab (`close-tab`, `go-to-tab-name`)
|
|
12
|
+
* silently no-op on a detached session with no attached client. Only
|
|
13
|
+
* `close-tab-by-id` works headlessly — so `open` captures the stable id
|
|
14
|
+
* that `new-tab` prints and persists a ticket -> id map for `close`.
|
|
15
|
+
* 2. `new-tab --layout` resolves a *file path* (not an inline string) and a
|
|
16
|
+
* per-tab layout does not inherit the session's tab-bar/status-bar, so we
|
|
17
|
+
* stage an absolute-path KDL file that includes the bar plugins itself.
|
|
18
|
+
* 3. There is no headless way to read a tab's command-exit state, so the
|
|
19
|
+
* agent command touches a marker file on exit that `list()` checks.
|
|
20
|
+
* 4. Zellij resurrects serialized sessions on attach; `open` drops a stale
|
|
21
|
+
* resurrectable groundcrew session before creating a fresh one so dead
|
|
22
|
+
* agent tabs don't reappear.
|
|
23
|
+
*/
|
|
24
|
+
import { type Adapter } from "./workspaceAdapter.ts";
|
|
25
|
+
export declare const zellijAdapter: Adapter;
|
|
26
|
+
//# sourceMappingURL=zellijAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zellijAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/zellijAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAgD/B,eAAO,MAAM,aAAa,EAAE,OAyF3B,CAAC"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zellij Workspace backend. Workspaces live as named tabs inside one
|
|
3
|
+
* dedicated `groundcrew` zellij session; the tab name is the ticket id and
|
|
4
|
+
* the first tab (`main`) is a placeholder that keeps the session alive. This
|
|
5
|
+
* is a Linux/WSL alternative to tmux: zellij is a stateful multiplexer, so it
|
|
6
|
+
* restores screen + terminal modes (mouse reporting, alt-screen) on every
|
|
7
|
+
* attach, and enables the mouse by default. Zellij can't paint status pills,
|
|
8
|
+
* so `open` silently drops `spec.status`.
|
|
9
|
+
*
|
|
10
|
+
* Zellij quirks shape this adapter:
|
|
11
|
+
* 1. Tab actions that target the *active* tab (`close-tab`, `go-to-tab-name`)
|
|
12
|
+
* silently no-op on a detached session with no attached client. Only
|
|
13
|
+
* `close-tab-by-id` works headlessly — so `open` captures the stable id
|
|
14
|
+
* that `new-tab` prints and persists a ticket -> id map for `close`.
|
|
15
|
+
* 2. `new-tab --layout` resolves a *file path* (not an inline string) and a
|
|
16
|
+
* per-tab layout does not inherit the session's tab-bar/status-bar, so we
|
|
17
|
+
* stage an absolute-path KDL file that includes the bar plugins itself.
|
|
18
|
+
* 3. There is no headless way to read a tab's command-exit state, so the
|
|
19
|
+
* agent command touches a marker file on exit that `list()` checks.
|
|
20
|
+
* 4. Zellij resurrects serialized sessions on attach; `open` drops a stale
|
|
21
|
+
* resurrectable groundcrew session before creating a fresh one so dead
|
|
22
|
+
* agent tabs don't reappear.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
|
|
28
|
+
import { debug, errorMessage, getLogFile, getLogRunStartByte, readEnvironmentVariable, } from "./util.js";
|
|
29
|
+
const ZELLIJ_SESSION = "groundcrew";
|
|
30
|
+
// The placeholder first tab; filtered out of `list()` like tmux's idle window.
|
|
31
|
+
const ZELLIJ_MAIN_TAB = "main";
|
|
32
|
+
// Zellij sessions are per-user and die on reboot, matching tmpdir lifetime —
|
|
33
|
+
// so a tmpdir-backed ticket -> stable-tab-id record stays in sync with reality.
|
|
34
|
+
// One file per ticket (rather than a shared JSON map) avoids a read-modify-write
|
|
35
|
+
// that concurrent open/close calls could lose or truncate. Overridable via env
|
|
36
|
+
// so tests can isolate it.
|
|
37
|
+
function tabIdDir() {
|
|
38
|
+
return (readEnvironmentVariable("GROUNDCREW_ZELLIJ_TAB_DIR") ??
|
|
39
|
+
path.join(tmpdir(), "groundcrew-zellij-tabs"));
|
|
40
|
+
}
|
|
41
|
+
function tabIdPath(name) {
|
|
42
|
+
return path.join(tabIdDir(), name.replaceAll(/[^a-zA-Z0-9_-]/g, "_"));
|
|
43
|
+
}
|
|
44
|
+
// Zellij exposes no headless way to read a tab's command-exit state (dump-layout
|
|
45
|
+
// doesn't distinguish exited from running, current-tab-info needs a client). So
|
|
46
|
+
// the agent command touches a per-ticket marker when it exits on its own; a
|
|
47
|
+
// groundcrew-issued close kills the process before the marker is written.
|
|
48
|
+
function exitMarkerDir() {
|
|
49
|
+
return (readEnvironmentVariable("GROUNDCREW_ZELLIJ_EXIT_DIR") ??
|
|
50
|
+
path.join(tmpdir(), "groundcrew-zellij-exited"));
|
|
51
|
+
}
|
|
52
|
+
function exitMarkerPath(name) {
|
|
53
|
+
return path.join(exitMarkerDir(), name.replaceAll(/[^a-zA-Z0-9_-]/g, "_"));
|
|
54
|
+
}
|
|
55
|
+
function clearExitMarker(name) {
|
|
56
|
+
rmSync(exitMarkerPath(name), { force: true });
|
|
57
|
+
}
|
|
58
|
+
export const zellijAdapter = {
|
|
59
|
+
async open(spec, signal) {
|
|
60
|
+
await ensureZellijSession(signal);
|
|
61
|
+
clearExitMarker(spec.name);
|
|
62
|
+
const layoutFile = stageTabLayout(spec.name, spec.command);
|
|
63
|
+
const output = await runWorkspaceCommand("zellij", [
|
|
64
|
+
"--session",
|
|
65
|
+
ZELLIJ_SESSION,
|
|
66
|
+
"action",
|
|
67
|
+
"new-tab",
|
|
68
|
+
"--name",
|
|
69
|
+
spec.name,
|
|
70
|
+
"--cwd",
|
|
71
|
+
spec.cwd,
|
|
72
|
+
"--layout",
|
|
73
|
+
layoutFile,
|
|
74
|
+
], signal);
|
|
75
|
+
const tabId = parseTabId(output);
|
|
76
|
+
if (tabId === undefined) {
|
|
77
|
+
debug(`zellij new-tab for ${spec.name} returned no parseable id: ${output}`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
rememberTabId(spec.name, tabId);
|
|
81
|
+
}
|
|
82
|
+
// Zellij can't paint status pills; spec.status is silently dropped.
|
|
83
|
+
},
|
|
84
|
+
async list(signal) {
|
|
85
|
+
let output;
|
|
86
|
+
try {
|
|
87
|
+
output = await runWorkspaceCommand("zellij", ["--session", ZELLIJ_SESSION, "action", "query-tab-names"], signal);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (isSignalAborted(signal)) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
// No groundcrew session => no workspaces (distinct from "couldn't ask").
|
|
94
|
+
if (isZellijMissingError(error)) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
debug(`zellij query-tab-names failed: ${errorMessage(error)}`);
|
|
98
|
+
// oxlint-disable-next-line unicorn/no-useless-undefined -- undefined marks the workspace backend as unavailable.
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return parseTabNames(output).map((workspace) => existsSync(exitMarkerPath(workspace.name))
|
|
102
|
+
? { name: workspace.name, state: "exited" }
|
|
103
|
+
: workspace);
|
|
104
|
+
},
|
|
105
|
+
async close(name, signal) {
|
|
106
|
+
const tabId = lookupTabId(name);
|
|
107
|
+
if (tabId === undefined) {
|
|
108
|
+
// Without the stable id we can't `close-tab-by-id`, and the active-tab
|
|
109
|
+
// close paths no-op headlessly. Treat as already gone.
|
|
110
|
+
debug(`zellij close: no tracked tab id for ${name}; treating as missing`);
|
|
111
|
+
clearExitMarker(name);
|
|
112
|
+
return { kind: "missing" };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await runWorkspaceCommand("zellij", ["--session", ZELLIJ_SESSION, "action", "close-tab-by-id", String(tabId)], signal);
|
|
116
|
+
forgetTabId(name);
|
|
117
|
+
clearExitMarker(name);
|
|
118
|
+
return { kind: "closed" };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (isSignalAborted(signal)) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
if (isZellijMissingError(error)) {
|
|
125
|
+
forgetTabId(name);
|
|
126
|
+
clearExitMarker(name);
|
|
127
|
+
return { kind: "missing" };
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
accessHint(_name) {
|
|
133
|
+
// Zellij attaches at the session level; the user clicks the ticket's tab.
|
|
134
|
+
return { kind: "attachCommand", command: `zellij attach ${ZELLIJ_SESSION}` };
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
async function ensureZellijSession(signal) {
|
|
138
|
+
const state = await zellijSessionState(signal);
|
|
139
|
+
if (state === "active") {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (state === "exited") {
|
|
143
|
+
// Zellij serializes sessions and resurrects them on attach; a stale
|
|
144
|
+
// resurrectable groundcrew session would replay dead agent tabs. Drop it
|
|
145
|
+
// so we start clean. (delete-session only acts on non-active sessions.)
|
|
146
|
+
try {
|
|
147
|
+
await runWorkspaceCommand("zellij", ["delete-session", ZELLIJ_SESSION], signal);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (isSignalAborted(signal)) {
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
debug(`zellij delete-session (stale) failed: ${errorMessage(error)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await runWorkspaceCommand("zellij", ["--layout", stageSessionLayout(), "attach", "--create-background", ZELLIJ_SESSION], signal);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (isSignalAborted(signal)) {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
// A racing creator may have won; tolerate that.
|
|
164
|
+
if ((await zellijSessionState(signal)) === "active") {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Whether the groundcrew session is live, resurrectable (serialized but not
|
|
172
|
+
* running), or absent. Parses `list-sessions -n` (no ANSI); an exited session
|
|
173
|
+
* is tagged `(EXITED - attach to resurrect)`.
|
|
174
|
+
*/
|
|
175
|
+
async function zellijSessionState(signal) {
|
|
176
|
+
let output;
|
|
177
|
+
try {
|
|
178
|
+
output = await runWorkspaceCommand("zellij", ["list-sessions", "-n"], signal);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (isSignalAborted(signal)) {
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
// `list-sessions` exits non-zero when there are no sessions at all.
|
|
185
|
+
return "absent";
|
|
186
|
+
}
|
|
187
|
+
for (const line of output.split("\n")) {
|
|
188
|
+
const trimmed = line.trim();
|
|
189
|
+
if (trimmed.split(/\s+/)[0] === ZELLIJ_SESSION) {
|
|
190
|
+
return trimmed.includes("EXITED") ? "exited" : "active";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return "absent";
|
|
194
|
+
}
|
|
195
|
+
function parseTabNames(output) {
|
|
196
|
+
const items = [];
|
|
197
|
+
for (const line of output.split("\n")) {
|
|
198
|
+
const name = line.trim();
|
|
199
|
+
if (name.length === 0 || name === ZELLIJ_MAIN_TAB) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
items.push({ name });
|
|
203
|
+
}
|
|
204
|
+
return items;
|
|
205
|
+
}
|
|
206
|
+
function parseTabId(output) {
|
|
207
|
+
// `new-tab` prints just the stable id; require the whole output to be that
|
|
208
|
+
// integer so we never latch onto a stray number from an unexpected message.
|
|
209
|
+
const trimmed = output.trim();
|
|
210
|
+
return /^\d+$/.test(trimmed) ? Number(trimmed) : undefined;
|
|
211
|
+
}
|
|
212
|
+
function isZellijMissingError(error) {
|
|
213
|
+
// Zellij phrases a missing/absent session several ways depending on whether
|
|
214
|
+
// other sessions exist: "Session 'groundcrew' not found", "There is no
|
|
215
|
+
// active session!", or "No active zellij sessions found". Scope the generic
|
|
216
|
+
// "not found" to our session so unrelated zellij errors aren't swallowed.
|
|
217
|
+
const message = errorMessage(error).toLowerCase();
|
|
218
|
+
return (message.includes("no active session") ||
|
|
219
|
+
message.includes("no active zellij sessions") ||
|
|
220
|
+
(message.includes("not found") && message.includes(ZELLIJ_SESSION)));
|
|
221
|
+
}
|
|
222
|
+
// --- staged KDL layouts ------------------------------------------------------
|
|
223
|
+
let stagingDir;
|
|
224
|
+
function staging() {
|
|
225
|
+
stagingDir ??= mkdtempSync(path.join(tmpdir(), "groundcrew-zellij-"));
|
|
226
|
+
return stagingDir;
|
|
227
|
+
}
|
|
228
|
+
/** Escapes a value for embedding inside a KDL double-quoted string. */
|
|
229
|
+
function kdlString(value) {
|
|
230
|
+
return value.replaceAll("\\", String.raw `\\`).replaceAll('"', String.raw `\"`);
|
|
231
|
+
}
|
|
232
|
+
/** Wraps a value in single quotes for a POSIX shell, escaping embedded quotes. */
|
|
233
|
+
function shSingleQuote(value) {
|
|
234
|
+
return `'${value.replaceAll("'", String.raw `'\''`)}'`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Session layout: bars on every tab via the template, plus the `main` tab.
|
|
238
|
+
* `main` tails the groundcrew log so attaching shows the live `crew run`
|
|
239
|
+
* orchestrator output; it falls back to a shell if the command exits or no
|
|
240
|
+
* log file is configured.
|
|
241
|
+
*/
|
|
242
|
+
function stageSessionLayout() {
|
|
243
|
+
const file = path.join(staging(), "session.kdl");
|
|
244
|
+
const logFile = getLogFile();
|
|
245
|
+
// `tail -c +N` shows only the current run (the log is shared/append-mode);
|
|
246
|
+
// N is the byte offset captured at process start, +1 to start just after it.
|
|
247
|
+
const fromByte = getLogRunStartByte() + 1;
|
|
248
|
+
const mainPane = logFile === undefined
|
|
249
|
+
? " pane\n"
|
|
250
|
+
: ` pane command="sh" {
|
|
251
|
+
args "-c" "${kdlString(`tail -c +${fromByte} -F ${shSingleQuote(logFile)} || exec \${SHELL:-sh}`)}"
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
writeFileSync(file, `layout {
|
|
255
|
+
default_tab_template {
|
|
256
|
+
pane size=1 borderless=true { plugin location="tab-bar"; }
|
|
257
|
+
children
|
|
258
|
+
pane size=1 borderless=true { plugin location="status-bar"; }
|
|
259
|
+
}
|
|
260
|
+
tab name="${ZELLIJ_MAIN_TAB}" {
|
|
261
|
+
${mainPane} }
|
|
262
|
+
}
|
|
263
|
+
`);
|
|
264
|
+
return file;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Per-ticket tab layout: bar plugins (a new tab does not inherit the session
|
|
268
|
+
* template) wrapping the agent command. Written to an absolute path because
|
|
269
|
+
* `new-tab --layout` resolves a file path, not an inline string.
|
|
270
|
+
*/
|
|
271
|
+
function stageTabLayout(ticket, command) {
|
|
272
|
+
const file = path.join(staging(), `tab-${ticket.replaceAll(/[^a-zA-Z0-9_-]/g, "_")}.kdl`);
|
|
273
|
+
// Touch the exit marker once the agent exits on its own, so `list()` can
|
|
274
|
+
// report the tab as exited. A groundcrew close kills the process first, so
|
|
275
|
+
// the marker is never written in that case.
|
|
276
|
+
try {
|
|
277
|
+
mkdirSync(exitMarkerDir(), { recursive: true });
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
/* v8 ignore next @preserve -- best-effort: a failed marker dir only disables exit-state detection */
|
|
281
|
+
debug(`zellij: could not create exit-marker dir: ${errorMessage(error)}`);
|
|
282
|
+
}
|
|
283
|
+
// An EXIT trap fires on any exit (including an explicit `exit` in the agent
|
|
284
|
+
// command), unlike a trailing `; touch`. The marker path is sanitized to
|
|
285
|
+
// [A-Za-z0-9_-] under tmpdir, so single-quoting it in the trap is safe.
|
|
286
|
+
const wrapped = `trap ${shSingleQuote(`touch ${exitMarkerPath(ticket)}`)} EXIT; ${command}`;
|
|
287
|
+
writeFileSync(file, `layout {
|
|
288
|
+
pane size=1 borderless=true { plugin location="tab-bar"; }
|
|
289
|
+
pane command="sh" {
|
|
290
|
+
args "-c" "${kdlString(wrapped)}"
|
|
291
|
+
}
|
|
292
|
+
pane size=1 borderless=true { plugin location="status-bar"; }
|
|
293
|
+
}
|
|
294
|
+
`);
|
|
295
|
+
return file;
|
|
296
|
+
}
|
|
297
|
+
// --- ticket -> stable tab id (one file per ticket; see tabIdDir) -------------
|
|
298
|
+
function rememberTabId(name, id) {
|
|
299
|
+
try {
|
|
300
|
+
mkdirSync(tabIdDir(), { recursive: true });
|
|
301
|
+
writeFileSync(tabIdPath(name), String(id));
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
debug(`zellij: failed to persist tab id for ${name}: ${errorMessage(error)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function lookupTabId(name) {
|
|
308
|
+
try {
|
|
309
|
+
return parseTabId(readFileSync(tabIdPath(name), "utf8"));
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function forgetTabId(name) {
|
|
316
|
+
rmSync(tabIdPath(name), { force: true });
|
|
317
|
+
}
|
package/docs/commands.md
CHANGED
|
@@ -100,7 +100,7 @@ crew status ENG-123
|
|
|
100
100
|
crew resume ENG-123
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
The command closes the cmux/tmux workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present, stop records that fact so status can show the preserved branch.
|
|
103
|
+
The command closes the cmux/tmux/zellij workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present, stop records that fact so status can show the preserved branch.
|
|
104
104
|
|
|
105
105
|
## Resume
|
|
106
106
|
|
package/docs/configuration.md
CHANGED
|
@@ -194,12 +194,12 @@ and hook contract.
|
|
|
194
194
|
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew start <TASK>` for unlabeled tasks. `crew run` ignores unlabeled tasks and does not apply this default. Must exist in `models.definitions`. If you enable only `codex`, set `default: "codex"`. |
|
|
195
195
|
| `models.definitions` | **required** | Enabled model set. Built-in keys (`claude`, `codex`) can use `{}` to opt into the shipped preset. Custom model names must provide `cmd` and `color`. |
|
|
196
196
|
| `models.definitions.<name>.cmd` | preset for built-ins | Shell command launched for the model. Required for custom models. Runs in the worktree through the resolved `local.runner`. `{{worktree}}` is replaced before launch; `{{sandbox}}` expands to the sbx sandbox name under the sdx runner and an empty string otherwise. |
|
|
197
|
-
| `models.definitions.<name>.color` | preset for built-ins | Color for the workspace status pill (cmux only; tmux silently
|
|
197
|
+
| `models.definitions.<name>.color` | preset for built-ins | Color for the workspace status pill (cmux only; tmux and zellij silently drop it). Required for custom models. |
|
|
198
198
|
| `models.definitions.<name>.usage` | preset for built-ins | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage` plus the weekly paced budget when codexbar exposes a weekly window. When `usage.codexbar.source` is omitted, groundcrew uses `oauth` for Codex/Claude on macOS, `auto` for other macOS providers, and `cli` elsewhere. Set to `{ disabled: true }` to disable usage gating while keeping the model enabled. |
|
|
199
199
|
| `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Field: `agent` (required sbx agent name). Groundcrew assumes the `groundcrew-<agent>` sandbox already exists. |
|
|
200
200
|
| `models.definitions.<name>.preLaunch` | optional | Host-only shell snippet run before the agent exec and outside Safehouse/sdx. Exports survive into the launch shell; under the default `safehouse` runner they are only forwarded to the agent when listed via `preLaunchEnv` or when `cmd` includes its own `safehouse --env-pass=NAMES`. `{{worktree}}` is substituted. A non-zero exit aborts launch. Not supported when `local.runner` resolves to `sdx` in v1. |
|
|
201
201
|
| `models.definitions.<name>.preLaunchEnv` | optional | Companion to `preLaunch`: list of env var names to append to groundcrew's `safehouse-clearance` `--env-pass=` flag, so `preLaunch` exports reach the agent without overriding `cmd` and losing the project's egress allowlist. Each entry must match `[A-Za-z_][A-Za-z0-9_]*`. Under `runner: "none"` exports already inherit and `preLaunchEnv` is a no-op. An empty array is a uniform no-op in every runner; a non-empty list is rejected when `cmd` already starts with `safehouse` or when `runner` resolves to `sdx`. |
|
|
202
202
|
| `prompts.initial` | unattended template | First message sent to the agent: the execution wrapper around each task. The task description is the task-specific prompt. Placeholders: `{{task}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. Override only to change the execution contract for every task, such as team-wide review rules or tool conventions. |
|
|
203
|
-
| `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"
|
|
203
|
+
| `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"`, `"tmux"`, or `"zellij"` to fail loudly when the chosen backend is missing. |
|
|
204
204
|
| `local.runner` | `"auto"` | Local isolation backend. `"auto"` uses `safehouse` on macOS and `sdx` on Linux/WSL. Explicit: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly. |
|
|
205
205
|
| `logging.file` | XDG state path | Append-mode log file. `log()` / `logEvent()` tee here in addition to stdout. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
|
package/docs/troubleshooting.md
CHANGED
|
@@ -43,6 +43,12 @@ Doctor reports the resolved local runner and whether its prerequisites are met,
|
|
|
43
43
|
|
|
44
44
|
Set `workspaceKind: "tmux"` to force the tmux backend when cmux's CLI/socket bridge is flaky, such as `cmux --json list-workspaces` returning `Failed to write to socket (Broken pipe)` or `Socket not found at ...cmux.sock` on every tick. Tmux is more reliable because it uses a unix socket, at the cost of losing cmux's status pills, notifications, and sidebar.
|
|
45
45
|
|
|
46
|
+
## Zellij Backend
|
|
47
|
+
|
|
48
|
+
Set `workspaceKind: "zellij"` to run agents as tabs in a shared `groundcrew` zellij session. Each ticket is a named tab; `main` tails the live `crew run` log. Attach with `zellij attach groundcrew` (the session is created on first dispatch, so it does not exist until a ticket runs). When an agent exits on its own its tab stays and `crew status` reports it as `exited`; a groundcrew-issued close removes the tab. groundcrew also drops a stale resurrectable `groundcrew` session on launch so dead agent tabs from a previous run are not replayed on attach.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
46
52
|
## Agent CLI Must Accept A Positional Prompt
|
|
47
53
|
|
|
48
54
|
The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` all support this.
|
package/package.json
CHANGED
|
Binary file
|