@clipboard-health/groundcrew 4.36.1 → 4.37.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/dist/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +9 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +9 -0
- package/dist/lib/adapters/todo-txt/fileErrors.d.ts +4 -0
- package/dist/lib/adapters/todo-txt/fileErrors.d.ts.map +1 -0
- package/dist/lib/adapters/todo-txt/fileErrors.js +20 -0
- package/dist/lib/adapters/todo-txt/source.d.ts.map +1 -1
- package/dist/lib/adapters/todo-txt/source.js +16 -5
- package/dist/lib/adapters/todo-txt/writeback.d.ts.map +1 -1
- package/dist/lib/adapters/todo-txt/writeback.js +10 -2
- package/dist/lib/agentLaunch.d.ts +1 -0
- package/dist/lib/agentLaunch.d.ts.map +1 -1
- package/dist/lib/agentLaunch.js +2 -0
- package/dist/lib/launchCommand.d.ts +5 -0
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +11 -2
- package/dist/lib/srtLaunch.d.ts +2 -0
- package/dist/lib/srtLaunch.d.ts.map +1 -1
- package/dist/lib/srtLaunch.js +1 -0
- package/dist/lib/srtPolicy.d.ts +5 -0
- package/dist/lib/srtPolicy.d.ts.map +1 -1
- package/dist/lib/srtPolicy.js +2 -0
- package/dist/lib/taskSourceFilesystem.d.ts +7 -0
- package/dist/lib/taskSourceFilesystem.d.ts.map +1 -0
- package/dist/lib/taskSourceFilesystem.js +39 -0
- package/dist/lib/tmuxAdapter.d.ts +22 -5
- package/dist/lib/tmuxAdapter.d.ts.map +1 -1
- package/dist/lib/tmuxAdapter.js +269 -89
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +16 -2
- package/docs/troubleshooting.md +15 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAgBnE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AA0ID,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAuFf;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
|
|
@@ -6,6 +6,7 @@ import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../l
|
|
|
6
6
|
import { workerEnvironmentForTask } from "../lib/launchCommand.js";
|
|
7
7
|
import { readRunState, recordRunState } from "../lib/runState.js";
|
|
8
8
|
import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
9
|
+
import { taskSourceWritePathsForCompletion } from "../lib/taskSourceFilesystem.js";
|
|
9
10
|
import { naturalIdFromCanonical, toCanonicalId } from "../lib/taskSource.js";
|
|
10
11
|
import { errorMessage, log } from "../lib/util.js";
|
|
11
12
|
import { workspaces } from "../lib/workspaces.js";
|
|
@@ -141,6 +142,13 @@ export async function resumeWorkspace(config, options) {
|
|
|
141
142
|
let srtSettingsDir;
|
|
142
143
|
try {
|
|
143
144
|
let launchCommand;
|
|
145
|
+
const taskSourceWritePaths = runner === "safehouse" || runner === "srt"
|
|
146
|
+
? taskSourceWritePathsForCompletion({
|
|
147
|
+
config,
|
|
148
|
+
taskId: context.completionTaskId,
|
|
149
|
+
workingDir: launchDir,
|
|
150
|
+
})
|
|
151
|
+
: undefined;
|
|
144
152
|
({ launchCommand, srtSettingsDir } = composeAgentLaunch({
|
|
145
153
|
runner,
|
|
146
154
|
task,
|
|
@@ -152,6 +160,7 @@ export async function resumeWorkspace(config, options) {
|
|
|
152
160
|
sandboxName,
|
|
153
161
|
workspaceKind,
|
|
154
162
|
workerEnvironment: workerEnvironmentForTask(context.completionTaskId),
|
|
163
|
+
taskSourceWritePaths,
|
|
155
164
|
}));
|
|
156
165
|
const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
|
|
157
166
|
await openAgentWorkspace({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAwBnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAwIf;AAgJD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA6Cf"}
|
|
@@ -7,6 +7,7 @@ import { workerEnvironmentForTask } from "../lib/launchCommand.js";
|
|
|
7
7
|
import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
|
|
8
8
|
import { recordRunState } from "../lib/runState.js";
|
|
9
9
|
import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
10
|
+
import { taskSourceWritePathsForCompletion } from "../lib/taskSourceFilesystem.js";
|
|
10
11
|
import { naturalIdFromCanonical } from "../lib/taskSource.js";
|
|
11
12
|
import { debug, errorMessage, log, okMark } from "../lib/util.js";
|
|
12
13
|
import { workspaces } from "../lib/workspaces.js";
|
|
@@ -82,6 +83,13 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
82
83
|
});
|
|
83
84
|
const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(promptDir);
|
|
84
85
|
const completionTaskId = options.completionTaskId ?? task;
|
|
86
|
+
const taskSourceWritePaths = runner === "safehouse" || runner === "srt"
|
|
87
|
+
? taskSourceWritePathsForCompletion({
|
|
88
|
+
config,
|
|
89
|
+
taskId: completionTaskId,
|
|
90
|
+
workingDir: launchDir,
|
|
91
|
+
})
|
|
92
|
+
: undefined;
|
|
85
93
|
const { launchCommand, srtSettingsDir: stagedSrtSettingsDir } = composeAgentLaunch({
|
|
86
94
|
runner,
|
|
87
95
|
task,
|
|
@@ -94,6 +102,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
94
102
|
sandboxName,
|
|
95
103
|
workspaceKind,
|
|
96
104
|
workerEnvironment: workerEnvironmentForTask(completionTaskId),
|
|
105
|
+
taskSourceWritePaths,
|
|
97
106
|
});
|
|
98
107
|
srtSettingsDir = stagedSrtSettingsDir;
|
|
99
108
|
const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileErrors.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/fileErrors.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMhE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAErE;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMxD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function fileErrorCode(error) {
|
|
2
|
+
/* v8 ignore next @preserve -- fs failures are Error objects carrying a string code */
|
|
3
|
+
if (!(error instanceof Error) || !errorHasCode(error)) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
return String(error.code);
|
|
7
|
+
}
|
|
8
|
+
export function isFileErrorCode(error, code) {
|
|
9
|
+
return fileErrorCode(error) === code;
|
|
10
|
+
}
|
|
11
|
+
function errorHasCode(error) {
|
|
12
|
+
return Object.hasOwn(error, "code");
|
|
13
|
+
}
|
|
14
|
+
export function describeFileError(error) {
|
|
15
|
+
const code = fileErrorCode(error);
|
|
16
|
+
/* v8 ignore next @preserve -- callers pass filesystem Error objects; fallback is defensive */
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
/* v8 ignore next @preserve -- filesystem Error objects carry a code; fallback is defensive */
|
|
19
|
+
return code === undefined ? message : `${code}: ${message}`;
|
|
20
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/source.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAKL,KAAK,UAAU,EAEhB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/source.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAKL,KAAK,UAAU,EAEhB,MAAM,qBAAqB,CAAC;AAM7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA2WxD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACtB,UAAU,CA2KZ"}
|
|
@@ -5,6 +5,7 @@ import { AGENT_ANY } from "../../config.js";
|
|
|
5
5
|
import { toCanonicalId, } from "../../taskSource.js";
|
|
6
6
|
import { formatKnownRepositories } from "../../repositoryValidation.js";
|
|
7
7
|
import { readEnvironmentVariable } from "../../util.js";
|
|
8
|
+
import { describeFileError, isFileErrorCode } from "./fileErrors.js";
|
|
8
9
|
import { isActiveForFetch, normalizeToIssue } from "./normalizer.js";
|
|
9
10
|
import { DATE_RE, getMetadataFirst, parseAllLines } from "./parser.js";
|
|
10
11
|
import { copyPromptFile, updateTaskStatus, validateTodoFile, withLock } from "./writeback.js";
|
|
@@ -33,8 +34,11 @@ function fileUpdatedAt(filePath) {
|
|
|
33
34
|
try {
|
|
34
35
|
return new Date(statSync(filePath).mtimeMs).toISOString();
|
|
35
36
|
}
|
|
36
|
-
catch {
|
|
37
|
-
/* v8 ignore next @preserve -- statSync failing means file missing; covered by empty-file tests */
|
|
37
|
+
catch (error) {
|
|
38
|
+
/* v8 ignore next @preserve -- statSync failing with ENOENT means file missing; covered by empty-file tests */
|
|
39
|
+
if (!isFileErrorCode(error, "ENOENT")) {
|
|
40
|
+
throw todoFileAccessError("stat", filePath, error);
|
|
41
|
+
}
|
|
38
42
|
return new Date().toISOString();
|
|
39
43
|
}
|
|
40
44
|
}
|
|
@@ -43,13 +47,19 @@ function readAndParseTodo(todoPath) {
|
|
|
43
47
|
try {
|
|
44
48
|
content = readFileSync(todoPath, "utf8");
|
|
45
49
|
}
|
|
46
|
-
catch {
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!isFileErrorCode(error, "ENOENT")) {
|
|
52
|
+
throw todoFileAccessError("read", todoPath, error);
|
|
53
|
+
}
|
|
47
54
|
content = "";
|
|
48
55
|
}
|
|
49
56
|
return {
|
|
50
57
|
parsedAll: parseAllLines(content),
|
|
51
58
|
};
|
|
52
59
|
}
|
|
60
|
+
function todoFileAccessError(operation, filePath, error) {
|
|
61
|
+
return new Error(`todo-txt: could not ${operation} todo file ${filePath}: ${describeFileError(error)}`);
|
|
62
|
+
}
|
|
53
63
|
function buildIssue(options) {
|
|
54
64
|
const { parsedIndex, parsedAll, sourceName, todoPath, tasksDir, defaultRepository, updatedAt } = options;
|
|
55
65
|
const parsed = parsedAll[parsedIndex];
|
|
@@ -255,8 +265,9 @@ function appendTodoLine(todoPath, line) {
|
|
|
255
265
|
separator = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
256
266
|
}
|
|
257
267
|
catch (error) {
|
|
258
|
-
/* v8 ignore
|
|
259
|
-
if (!(error
|
|
268
|
+
/* v8 ignore next @preserve -- non-ENOENT append failures are an fs passthrough, not todo-txt logic */
|
|
269
|
+
if (!isFileErrorCode(error, "ENOENT")) {
|
|
270
|
+
/* v8 ignore next @preserve -- non-ENOENT append failures are an fs passthrough, not todo-txt logic */
|
|
260
271
|
throw error;
|
|
261
272
|
}
|
|
262
273
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/writeback.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/writeback.ts"],"names":[],"mappings":"AAaA,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAqOD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,gBAAgB,CAAC;IACtB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAQxF;AAED,KAAK,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,MAAM,CAAC;AAwF3D,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,cAAc,GACxB,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA+ElC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAQrE;AAsHD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAChC,MAAM,EAAE,CA0CV"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { describeFileError, fileErrorCode } from "./fileErrors.js";
|
|
3
4
|
import { DATE_RE, hashLine, parseAllLines } from "./parser.js";
|
|
4
5
|
import { isValidThresholdValue } from "./normalizer.js";
|
|
5
6
|
function isoDate(date) {
|
|
@@ -334,6 +335,13 @@ export function copyPromptFile(oldPath, newPath) {
|
|
|
334
335
|
// prompt file is optional — copy is best-effort
|
|
335
336
|
}
|
|
336
337
|
}
|
|
338
|
+
function todoFileReadError(todoPath, error) {
|
|
339
|
+
const code = fileErrorCode(error);
|
|
340
|
+
if (code === "ENOENT") {
|
|
341
|
+
return `missing todo file: ${todoPath}`;
|
|
342
|
+
}
|
|
343
|
+
return `could not read todo file ${todoPath}: ${describeFileError(error)}`;
|
|
344
|
+
}
|
|
337
345
|
function validatePromptFile(tasksDir, id, promptOverride, title, prefix, errors) {
|
|
338
346
|
const promptPath = promptOverride ?? path.join(tasksDir, `${id}.md`);
|
|
339
347
|
const shouldRequirePrompt = promptOverride !== undefined || title.trim().length === 0;
|
|
@@ -405,8 +413,8 @@ export function validateTodoFile(todoPath, tasksDir, knownAgents) {
|
|
|
405
413
|
try {
|
|
406
414
|
content = readFileSync(todoPath, "utf8");
|
|
407
415
|
}
|
|
408
|
-
catch {
|
|
409
|
-
return [
|
|
416
|
+
catch (error) {
|
|
417
|
+
return [todoFileReadError(todoPath, error)];
|
|
410
418
|
}
|
|
411
419
|
const parsedAll = parseAllLines(content);
|
|
412
420
|
const idsSeen = new Map();
|
|
@@ -20,6 +20,7 @@ export declare function composeAgentLaunch(input: {
|
|
|
20
20
|
sandboxName?: string | undefined;
|
|
21
21
|
workspaceKind: WorkspaceKind;
|
|
22
22
|
workerEnvironment?: WorkerEnvironment | undefined;
|
|
23
|
+
taskSourceWritePaths?: readonly string[] | undefined;
|
|
23
24
|
}): {
|
|
24
25
|
launchCommand: string;
|
|
25
26
|
srtSettingsDir: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAClD,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CACtD,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAmChE;AAmDD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;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,CAsD/B;AAwBD,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"}
|
package/dist/lib/agentLaunch.js
CHANGED
|
@@ -21,6 +21,7 @@ export function composeAgentLaunch(input) {
|
|
|
21
21
|
task: input.task,
|
|
22
22
|
worktreeDir: input.worktreeDir,
|
|
23
23
|
definition: input.definition,
|
|
24
|
+
taskSourceWritePaths: input.taskSourceWritePaths,
|
|
24
25
|
})
|
|
25
26
|
: undefined;
|
|
26
27
|
const safehouseAgentIntegration = input.runner === "safehouse"
|
|
@@ -41,6 +42,7 @@ export function composeAgentLaunch(input) {
|
|
|
41
42
|
srtAgentConfigDirEnv: staged?.agentConfigDirEnv,
|
|
42
43
|
workerEnvironment: input.workerEnvironment,
|
|
43
44
|
safehouseAddDirs: input.runner === "safehouse" ? resolveSafehouseAddDirs(input.worktreeDir) : undefined,
|
|
45
|
+
safehouseAgentAddDirs: input.runner === "safehouse" ? (input.taskSourceWritePaths ?? []) : undefined,
|
|
44
46
|
safehouseAgentIntegration,
|
|
45
47
|
});
|
|
46
48
|
return { launchCommand, srtSettingsDir: staged?.directory };
|
|
@@ -120,6 +120,11 @@ interface LaunchCommandArguments {
|
|
|
120
120
|
* pre-existing behavior). Only consumed by the safehouse wrap.
|
|
121
121
|
*/
|
|
122
122
|
safehouseAddDirs?: readonly string[] | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Extra read/write paths granted only to the Safehouse agent wrap. These are
|
|
125
|
+
* intentionally withheld from the repo-controlled prepareWorktree wrap.
|
|
126
|
+
*/
|
|
127
|
+
safehouseAgentAddDirs?: readonly string[] | undefined;
|
|
123
128
|
/**
|
|
124
129
|
* Extra host-terminal integration surface granted only to the Safehouse agent
|
|
125
130
|
* wrap. The agent may need to execute host shims and reach their sockets
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAIrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAgB3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,MAAM,CAMvF;AAsMD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;AAED,QAAA,MAAM,wBAAwB,YAAI,oBAAoB,EAAE,qBAAqB,CAAU,CAAC;AAExF,KAAK,qBAAqB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC;AAEhF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAK1E;AAsBD,MAAM,WAAW,yBAAyB;IACxC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IACnE;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACjD;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,yBAAyB,GAAG,SAAS,CAAC;IAClE;;;OAGG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;CACnD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CAkC7E"}
|
|
1
|
+
{"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAIrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAgB3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,MAAM,CAMvF;AAsMD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;AAED,QAAA,MAAM,wBAAwB,YAAI,oBAAoB,EAAE,qBAAqB,CAAU,CAAC;AAExF,KAAK,qBAAqB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC;AAEhF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAK1E;AAsBD,MAAM,WAAW,yBAAyB;IACxC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IACnE;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACjD;;;OAGG;IACH,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACtD;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,yBAAyB,GAAG,SAAS,CAAC;IAClE;;;OAGG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;CACnD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CAkC7E"}
|
|
@@ -404,7 +404,13 @@ function buildSafehouseLaunchCommand(arguments_) {
|
|
|
404
404
|
// Quote the whole value so shell-special chars survive; the trailing space
|
|
405
405
|
// separates it from the next argv token. See `resolveSafehouseAddDirs` for
|
|
406
406
|
// which paths these are and why.
|
|
407
|
-
const
|
|
407
|
+
const safehousePrepareAddDirs = arguments_.safehouseAddDirs ?? [];
|
|
408
|
+
const safehouseAgentAddDirs = uniqueStrings([
|
|
409
|
+
...safehousePrepareAddDirs,
|
|
410
|
+
...(arguments_.safehouseAgentAddDirs ?? []),
|
|
411
|
+
]);
|
|
412
|
+
const safehouseAddDirsFlag = safehousePathListFlag("--add-dirs", safehousePrepareAddDirs);
|
|
413
|
+
const safehouseAgentAddDirsFlag = safehousePathListFlag("--add-dirs", safehouseAgentAddDirs);
|
|
408
414
|
const safehouseAgentAddDirsReadOnlyFlag = safehousePathListFlag("--add-dirs-ro", safehouseAgentIntegration?.addDirsReadOnly ?? []);
|
|
409
415
|
const safehouseWrapper = safehouseClearanceWrapperCommand();
|
|
410
416
|
// Defensive shim+promptDir trap: by the time we arm it, `rm -rf <promptDir>`
|
|
@@ -435,12 +441,15 @@ function buildSafehouseLaunchCommand(arguments_) {
|
|
|
435
441
|
// Running the real launch chain as `sh -c` would make it see `sh`, so use
|
|
436
442
|
// an agent-named symlink to /bin/sh. This preserves per-agent profile
|
|
437
443
|
// selection without enabling every agent profile.
|
|
438
|
-
`{ ${safehouseWrapper} ${
|
|
444
|
+
`{ ${safehouseWrapper} ${safehouseAgentAddDirsFlag}${safehouseAgentAddDirsReadOnlyFlag}${agentEnvPassFlag}"$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh "$_p"; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"; }`);
|
|
439
445
|
return lines.join(" && ");
|
|
440
446
|
}
|
|
441
447
|
function safehousePathListFlag(flagName, paths) {
|
|
442
448
|
return paths.length === 0 ? "" : `${flagName}=${shellSingleQuote(paths.join(":"))} `;
|
|
443
449
|
}
|
|
450
|
+
function uniqueStrings(values) {
|
|
451
|
+
return [...new Set(values)];
|
|
452
|
+
}
|
|
444
453
|
/**
|
|
445
454
|
* Benign baseline env the srt wraps run under (via `env -i`). This is an
|
|
446
455
|
* allowlist on purpose: srt's CLI spawns its child with the *inherited* host
|
package/dist/lib/srtLaunch.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export declare function buildAndStageSrtLaunch(input: {
|
|
|
51
51
|
task: string;
|
|
52
52
|
worktreeDir: string;
|
|
53
53
|
definition: AgentDefinition;
|
|
54
|
+
/** Local task-source directories the agent needs for self-completion writeback. */
|
|
55
|
+
taskSourceWritePaths?: readonly string[] | undefined;
|
|
54
56
|
/** Defaults to `os.homedir()`. Injected in tests to seed from a fixture home. */
|
|
55
57
|
homeDir?: string;
|
|
56
58
|
}): StagedSrtLaunch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACrD,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,CA8ClB"}
|
package/dist/lib/srtLaunch.js
CHANGED
|
@@ -75,6 +75,7 @@ export function buildAndStageSrtLaunch(input) {
|
|
|
75
75
|
const agentSettings = buildSrtSettings({
|
|
76
76
|
...base,
|
|
77
77
|
agent,
|
|
78
|
+
taskSourceWritePaths: input.taskSourceWritePaths ?? [],
|
|
78
79
|
...(relocatedConfigDir === undefined ? {} : { relocatedConfigDir }),
|
|
79
80
|
});
|
|
80
81
|
const prepareFile = path.join(directory, "prepare-settings.json");
|
package/dist/lib/srtPolicy.d.ts
CHANGED
|
@@ -64,6 +64,11 @@ export interface BuildSrtSettingsInput {
|
|
|
64
64
|
* to the default config dir), so this is omitted for it.
|
|
65
65
|
*/
|
|
66
66
|
relocatedConfigDir?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Local task-source directories the worker needs for self-completion
|
|
69
|
+
* writeback, such as a todo-txt file's parent directory and task prompt dir.
|
|
70
|
+
*/
|
|
71
|
+
taskSourceWritePaths?: readonly string[];
|
|
67
72
|
/** Defaults to `process.platform`. Injected in tests to exercise both deny-read roots. */
|
|
68
73
|
platform?: NodeJS.Platform;
|
|
69
74
|
/** Defaults to `os.homedir()`. Injected in tests. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"srtPolicy.d.ts","sourceRoot":"","sources":["../../src/lib/srtPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAMH,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,qBAAqB,EAAE,MAAM,CAAC;IAC9B,kFAAkF;IAClF,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAEtF;AAsMD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,oBAAoB,
|
|
1
|
+
{"version":3,"file":"srtPolicy.d.ts","sourceRoot":"","sources":["../../src/lib/srtPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAMH,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,qBAAqB,EAAE,MAAM,CAAC;IAC9B,kFAAkF;IAClF,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAEtF;AAsMD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,oBAAoB,CAyHnF"}
|
package/dist/lib/srtPolicy.js
CHANGED
|
@@ -251,6 +251,7 @@ export function buildSrtSettings(input) {
|
|
|
251
251
|
...profile.readPaths.map(underHome),
|
|
252
252
|
...keychainRead,
|
|
253
253
|
...(input.relocatedConfigDir === undefined ? [] : [input.relocatedConfigDir]),
|
|
254
|
+
...(input.taskSourceWritePaths ?? []),
|
|
254
255
|
]);
|
|
255
256
|
const allowWrite = unique([
|
|
256
257
|
input.worktreeDir,
|
|
@@ -262,6 +263,7 @@ export function buildSrtSettings(input) {
|
|
|
262
263
|
// The agent's relocated, writable config home (codex). Absent for agents
|
|
263
264
|
// that write their real home behind a deny-list (claude).
|
|
264
265
|
...(input.relocatedConfigDir === undefined ? [] : [input.relocatedConfigDir]),
|
|
266
|
+
...(input.taskSourceWritePaths ?? []),
|
|
265
267
|
]);
|
|
266
268
|
const denyWrite = unique([
|
|
267
269
|
nodeGlobalModules,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
2
|
+
export declare function taskSourceWritePathsForCompletion(input: {
|
|
3
|
+
config: Pick<ResolvedConfig, "sources">;
|
|
4
|
+
taskId: string;
|
|
5
|
+
workingDir: string;
|
|
6
|
+
}): readonly string[];
|
|
7
|
+
//# sourceMappingURL=taskSourceFilesystem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taskSourceFilesystem.d.ts","sourceRoot":"","sources":["../../src/lib/taskSourceFilesystem.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAUlD,wBAAgB,iCAAiC,CAAC,KAAK,EAAE;IACvD,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,SAAS,MAAM,EAAE,CAwBpB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { todoTxtAdapterConfigSchema } from "./adapters/todo-txt/schema.js";
|
|
4
|
+
const sourceSelectorShape = z.looseObject({
|
|
5
|
+
kind: z.string().optional(),
|
|
6
|
+
name: z.string().optional(),
|
|
7
|
+
enabled: z.boolean().optional(),
|
|
8
|
+
});
|
|
9
|
+
const DEFAULT_TODO_SOURCE_NAME = "todo";
|
|
10
|
+
export function taskSourceWritePathsForCompletion(input) {
|
|
11
|
+
const targetSourceName = sourceNameFromTaskId(input.taskId);
|
|
12
|
+
const paths = [];
|
|
13
|
+
for (const rawSource of input.config.sources) {
|
|
14
|
+
const selector = sourceSelectorShape.parse(rawSource);
|
|
15
|
+
if (selector.enabled === false || selector.kind !== "todo-txt") {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const sourceName = selector.name ?? DEFAULT_TODO_SOURCE_NAME;
|
|
19
|
+
if (targetSourceName !== undefined && sourceName !== targetSourceName) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const source = todoTxtAdapterConfigSchema.parse(rawSource);
|
|
23
|
+
// Completion writeback writes the todo file plus lock/tmp siblings, so the
|
|
24
|
+
// sandbox grant must cover the todo file's parent directory.
|
|
25
|
+
paths.push(resolveForWorker(input.workingDir, path.dirname(source.todoPath)));
|
|
26
|
+
paths.push(resolveForWorker(input.workingDir, source.tasksDir));
|
|
27
|
+
}
|
|
28
|
+
return [...new Set(paths)];
|
|
29
|
+
}
|
|
30
|
+
function sourceNameFromTaskId(taskId) {
|
|
31
|
+
const colonIndex = taskId.indexOf(":");
|
|
32
|
+
if (colonIndex <= 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return taskId.slice(0, colonIndex);
|
|
36
|
+
}
|
|
37
|
+
function resolveForWorker(workingDir, filePath) {
|
|
38
|
+
return path.resolve(workingDir, filePath);
|
|
39
|
+
}
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tmux Workspace backend.
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* tmux Workspace backend. Two layouts, chosen by the caller via
|
|
3
|
+
* `createTmuxAdapter({ sessionPerTask })`. `workspaces.ts` resolves the flag
|
|
4
|
+
* from the `GROUNDCREW_TMUX_SESSION_PER_TASK` env var.
|
|
5
|
+
*
|
|
6
|
+
* - Window model (`sessionPerTask: false`): workspaces live as windows inside
|
|
7
|
+
* one dedicated `groundcrew` tmux session; the window name is the task id.
|
|
8
|
+
* - Session model (`sessionPerTask: true`): each workspace is its own dedicated
|
|
9
|
+
* tmux session named after the task id, so windows act as tabs and panes as
|
|
10
|
+
* splits without polluting a shared session. Sessions we create are tagged
|
|
11
|
+
* with the `@groundcrew_managed` user option so `list`/`close` ignore the
|
|
12
|
+
* user's own same-named sessions.
|
|
13
|
+
*
|
|
14
|
+
* Either way tmux can't paint status pills, so `open` silently drops
|
|
15
|
+
* `spec.status`. This is the Linux/WSL path where cmux is unavailable.
|
|
6
16
|
*/
|
|
7
17
|
import { type Adapter } from "./workspaceAdapter.ts";
|
|
8
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Builds the tmux adapter for the resolved layout. `sessionPerTask` is decided
|
|
20
|
+
* by `workspaces.ts` from the `GROUNDCREW_TMUX_SESSION_PER_TASK` env var, so
|
|
21
|
+
* the adapter itself stays config-agnostic.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createTmuxAdapter({ sessionPerTask }: {
|
|
24
|
+
sessionPerTask: boolean;
|
|
25
|
+
}): Adapter;
|
|
9
26
|
//# sourceMappingURL=tmuxAdapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/tmuxAdapter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/tmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,OAAO,EAMb,MAAM,uBAAuB,CAAC;AAqB/B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,cAAc,EAAE,EAAE;IAAE,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAiB1F"}
|
package/dist/lib/tmuxAdapter.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tmux Workspace backend.
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* tmux Workspace backend. Two layouts, chosen by the caller via
|
|
3
|
+
* `createTmuxAdapter({ sessionPerTask })`. `workspaces.ts` resolves the flag
|
|
4
|
+
* from the `GROUNDCREW_TMUX_SESSION_PER_TASK` env var.
|
|
5
|
+
*
|
|
6
|
+
* - Window model (`sessionPerTask: false`): workspaces live as windows inside
|
|
7
|
+
* one dedicated `groundcrew` tmux session; the window name is the task id.
|
|
8
|
+
* - Session model (`sessionPerTask: true`): each workspace is its own dedicated
|
|
9
|
+
* tmux session named after the task id, so windows act as tabs and panes as
|
|
10
|
+
* splits without polluting a shared session. Sessions we create are tagged
|
|
11
|
+
* with the `@groundcrew_managed` user option so `list`/`close` ignore the
|
|
12
|
+
* user's own same-named sessions.
|
|
13
|
+
*
|
|
14
|
+
* Either way tmux can't paint status pills, so `open` silently drops
|
|
15
|
+
* `spec.status`. This is the Linux/WSL path where cmux is unavailable.
|
|
6
16
|
*/
|
|
7
17
|
import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
|
|
8
18
|
import { debug, errorMessage, readEnvironmentVariable } from "./util.js";
|
|
@@ -13,99 +23,87 @@ const TMUX_SESSION = "groundcrew";
|
|
|
13
23
|
// sentinel and filter it out — it stays around as a placeholder so the
|
|
14
24
|
// session doesn't collapse when the last task window closes.
|
|
15
25
|
const TMUX_IDLE_WINDOW = "_groundcrew_idle";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
async list(signal) {
|
|
47
|
-
const probe = await probeTmuxList("#{window_name}\t#{pane_dead}", signal);
|
|
48
|
-
if (probe.status === "missing") {
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
if (probe.status === "failed") {
|
|
52
|
-
debug(`tmux list-windows failed: ${probe.reason}`);
|
|
53
|
-
// oxlint-disable-next-line unicorn/no-useless-undefined -- undefined marks the workspace backend as unavailable.
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
return parseTmuxWindows(probe.output, { includeExited: shouldKeepDeadWindows() });
|
|
57
|
-
},
|
|
58
|
-
async close(name, signal) {
|
|
59
|
-
try {
|
|
60
|
-
await runWorkspaceCommand("tmux", ["kill-window", "-t", tmuxTarget(name)], signal);
|
|
61
|
-
return { kind: "closed" };
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
if (isSignalAborted(signal)) {
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
if (isTmuxNotFoundError(error)) {
|
|
68
|
-
return { kind: "missing" };
|
|
69
|
-
}
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
accessHint(name) {
|
|
74
|
-
return { kind: "attachCommand", command: `tmux attach -t ${tmuxTarget(name)}` };
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
function tmuxTarget(name) {
|
|
78
|
-
return `${TMUX_SESSION}:${name}`;
|
|
26
|
+
// User option stamped on every session we create in the session model, so
|
|
27
|
+
// `list`/`close` can tell our sessions apart from the user's own (task-id
|
|
28
|
+
// session names carry no prefix and could collide).
|
|
29
|
+
const MANAGED_OPTION = "@groundcrew_managed";
|
|
30
|
+
// One row per window across every session: session name, our managed tag
|
|
31
|
+
// (empty for sessions we didn't create), and the active pane's dead flag.
|
|
32
|
+
const SESSION_PROBE_FORMAT = `#{session_name}\t#{${MANAGED_OPTION}}\t#{pane_dead}`;
|
|
33
|
+
/**
|
|
34
|
+
* Builds the tmux adapter for the resolved layout. `sessionPerTask` is decided
|
|
35
|
+
* by `workspaces.ts` from the `GROUNDCREW_TMUX_SESSION_PER_TASK` env var, so
|
|
36
|
+
* the adapter itself stays config-agnostic.
|
|
37
|
+
*/
|
|
38
|
+
export function createTmuxAdapter({ sessionPerTask }) {
|
|
39
|
+
return {
|
|
40
|
+
async open(spec, signal) {
|
|
41
|
+
await (sessionPerTask ? openSession(spec, signal) : openWindow(spec, signal));
|
|
42
|
+
// tmux can't paint status pills; spec.status is silently dropped.
|
|
43
|
+
},
|
|
44
|
+
async list(signal) {
|
|
45
|
+
return await (sessionPerTask ? listSessions(signal) : listWindows(signal));
|
|
46
|
+
},
|
|
47
|
+
async close(name, signal) {
|
|
48
|
+
return await (sessionPerTask ? closeSession(name, signal) : closeWindow(name, signal));
|
|
49
|
+
},
|
|
50
|
+
accessHint(name) {
|
|
51
|
+
const target = sessionPerTask ? name : tmuxTarget(name);
|
|
52
|
+
return { kind: "attachCommand", command: `tmux attach -t ${target}` };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
79
55
|
}
|
|
80
56
|
function shouldKeepDeadWindows() {
|
|
81
57
|
const keepDeadWindowsEnv = readEnvironmentVariable("GROUNDCREW_KEEP_DEAD_WINDOWS");
|
|
82
58
|
return keepDeadWindowsEnv === "1";
|
|
83
59
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Window model (default): one window per task inside the `groundcrew` session.
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
async function openWindow(spec, signal) {
|
|
64
|
+
await ensureTmuxSession(signal);
|
|
65
|
+
const target = tmuxTarget(spec.name);
|
|
66
|
+
const keepDeadWindows = shouldKeepDeadWindows();
|
|
67
|
+
await runWorkspaceCommand("tmux", [
|
|
68
|
+
"new-window",
|
|
69
|
+
"-d",
|
|
70
|
+
"-t",
|
|
71
|
+
TMUX_SESSION,
|
|
72
|
+
"-n",
|
|
73
|
+
spec.name,
|
|
74
|
+
"-c",
|
|
75
|
+
spec.cwd,
|
|
76
|
+
spec.command,
|
|
77
|
+
";",
|
|
78
|
+
"set-window-option",
|
|
79
|
+
"-t",
|
|
80
|
+
target,
|
|
81
|
+
"remain-on-exit",
|
|
82
|
+
keepDeadWindows ? "on" : "off",
|
|
83
|
+
";",
|
|
84
|
+
"set-window-option",
|
|
85
|
+
"-t",
|
|
86
|
+
target,
|
|
87
|
+
"allow-rename",
|
|
88
|
+
"off",
|
|
89
|
+
], signal);
|
|
92
90
|
}
|
|
93
|
-
async function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
output: await runWorkspaceCommand("tmux", ["list-windows", "-t", TMUX_SESSION, "-F", format], signal),
|
|
98
|
-
};
|
|
91
|
+
async function listWindows(signal) {
|
|
92
|
+
const probe = await probeTmuxCommand(["list-windows", "-t", TMUX_SESSION, "-F", "#{window_name}\t#{pane_dead}"], signal);
|
|
93
|
+
if (probe.status === "missing") {
|
|
94
|
+
return [];
|
|
99
95
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
if (isTmuxNotFoundError(error)) {
|
|
105
|
-
return { status: "missing" };
|
|
106
|
-
}
|
|
107
|
-
return { status: "failed", reason: errorMessage(error) };
|
|
96
|
+
if (probe.status === "failed") {
|
|
97
|
+
debug(`tmux list-windows failed: ${probe.reason}`);
|
|
98
|
+
return undefined;
|
|
108
99
|
}
|
|
100
|
+
return parseTmuxWindows(probe.output, { includeExited: shouldKeepDeadWindows() });
|
|
101
|
+
}
|
|
102
|
+
async function closeWindow(name, signal) {
|
|
103
|
+
return await killTmuxTarget(["kill-window", "-t", tmuxTarget(name)], signal);
|
|
104
|
+
}
|
|
105
|
+
function tmuxTarget(name) {
|
|
106
|
+
return `${TMUX_SESSION}:${name}`;
|
|
109
107
|
}
|
|
110
108
|
async function ensureTmuxSession(signal) {
|
|
111
109
|
try {
|
|
@@ -158,3 +156,185 @@ function parseTmuxWindows(output, options = {}) {
|
|
|
158
156
|
}
|
|
159
157
|
return items;
|
|
160
158
|
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Session model: one dedicated session per task, tagged @groundcrew_managed.
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
async function openSession(spec, signal) {
|
|
163
|
+
try {
|
|
164
|
+
await createSession(spec, signal);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (isSignalAborted(signal)) {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
if (!isTmuxDuplicateSessionError(error)) {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
// A session of this name already exists. Only recreate it if it's one of
|
|
174
|
+
// ours; never clobber a same-named session the user opened themselves.
|
|
175
|
+
const probe = await probeManagedSessions(signal);
|
|
176
|
+
if (probe.status !== "ok" || !probe.sessions.has(spec.name)) {
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
await runWorkspaceCommand("tmux", ["kill-session", "-t", spec.name], signal);
|
|
180
|
+
try {
|
|
181
|
+
await createSession(spec, signal);
|
|
182
|
+
}
|
|
183
|
+
catch (recreateError) {
|
|
184
|
+
if (isSignalAborted(signal)) {
|
|
185
|
+
throw recreateError;
|
|
186
|
+
}
|
|
187
|
+
// We already killed a stale copy; a failure here (e.g. the session was
|
|
188
|
+
// recreated in the gap) is unexpected, so surface it with context rather
|
|
189
|
+
// than a bare tmux message or an unbounded kill/recreate loop.
|
|
190
|
+
throw new Error(`Failed to recreate tmux session "${spec.name}" after killing a stale copy: ${errorMessage(recreateError)}`, { cause: recreateError });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function createSession(spec, signal) {
|
|
195
|
+
const keepDeadWindows = shouldKeepDeadWindows();
|
|
196
|
+
await runWorkspaceCommand("tmux", [
|
|
197
|
+
"new-session",
|
|
198
|
+
"-d",
|
|
199
|
+
"-s",
|
|
200
|
+
spec.name,
|
|
201
|
+
"-c",
|
|
202
|
+
spec.cwd,
|
|
203
|
+
spec.command,
|
|
204
|
+
";",
|
|
205
|
+
"set-option",
|
|
206
|
+
"-t",
|
|
207
|
+
spec.name,
|
|
208
|
+
MANAGED_OPTION,
|
|
209
|
+
"1",
|
|
210
|
+
";",
|
|
211
|
+
"set-window-option",
|
|
212
|
+
"-t",
|
|
213
|
+
spec.name,
|
|
214
|
+
"remain-on-exit",
|
|
215
|
+
keepDeadWindows ? "on" : "off",
|
|
216
|
+
";",
|
|
217
|
+
"set-window-option",
|
|
218
|
+
"-t",
|
|
219
|
+
spec.name,
|
|
220
|
+
"allow-rename",
|
|
221
|
+
"off",
|
|
222
|
+
], signal);
|
|
223
|
+
}
|
|
224
|
+
async function listSessions(signal) {
|
|
225
|
+
const probe = await probeManagedSessions(signal);
|
|
226
|
+
if (probe.status === "missing") {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
if (probe.status === "failed") {
|
|
230
|
+
debug(`tmux list-windows -a failed: ${probe.reason}`);
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const includeExited = shouldKeepDeadWindows();
|
|
234
|
+
const items = [];
|
|
235
|
+
for (const [name, hasLivePane] of probe.sessions) {
|
|
236
|
+
if (hasLivePane) {
|
|
237
|
+
items.push({ name });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
// No live pane left: the agent command exited but the session lingers
|
|
241
|
+
// because remain-on-exit kept the dead pane around. Mirror the window
|
|
242
|
+
// model and only surface it when callers opted into keeping dead windows.
|
|
243
|
+
if (includeExited) {
|
|
244
|
+
items.push({ name, state: "exited" });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return items;
|
|
248
|
+
}
|
|
249
|
+
async function closeSession(name, signal) {
|
|
250
|
+
const probe = await probeManagedSessions(signal);
|
|
251
|
+
if (probe.status === "missing") {
|
|
252
|
+
return { kind: "missing" };
|
|
253
|
+
}
|
|
254
|
+
if (probe.status === "failed") {
|
|
255
|
+
// Can't confirm ownership; refuse rather than risk killing a user session.
|
|
256
|
+
debug(`tmux kill-session skipped for ${name}: list-windows -a failed: ${probe.reason}`);
|
|
257
|
+
return { kind: "unavailable" };
|
|
258
|
+
}
|
|
259
|
+
if (!probe.sessions.has(name)) {
|
|
260
|
+
return { kind: "missing" };
|
|
261
|
+
}
|
|
262
|
+
return await killTmuxTarget(["kill-session", "-t", name], signal);
|
|
263
|
+
}
|
|
264
|
+
async function probeManagedSessions(signal) {
|
|
265
|
+
const probe = await probeTmuxCommand(["list-windows", "-a", "-F", SESSION_PROBE_FORMAT], signal);
|
|
266
|
+
if (probe.status === "ok") {
|
|
267
|
+
return { status: "ok", sessions: parseManagedSessions(probe.output) };
|
|
268
|
+
}
|
|
269
|
+
return probe;
|
|
270
|
+
}
|
|
271
|
+
function parseManagedSessions(output) {
|
|
272
|
+
const sessions = new Map();
|
|
273
|
+
for (const line of output.split("\n")) {
|
|
274
|
+
if (line.length === 0) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const [name, managedFlag, deadFlag] = line.split("\t");
|
|
278
|
+
/* v8 ignore next 3 @preserve -- split on a non-empty string always yields a non-empty first element */
|
|
279
|
+
if (name === undefined || name.length === 0) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Only sessions we stamped carry the tag; everything else is the user's.
|
|
283
|
+
if (managedFlag !== "1") {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// Mirror parseTmuxWindows: a pane is live unless tmux explicitly reports it
|
|
287
|
+
// dead. A missing field (malformed row) counts as live, not exited, so both
|
|
288
|
+
// models read identical output the same way.
|
|
289
|
+
const isDeadPane = deadFlag !== undefined && deadFlag !== "0";
|
|
290
|
+
sessions.set(name, (sessions.get(name) ?? false) || !isDeadPane);
|
|
291
|
+
}
|
|
292
|
+
return sessions;
|
|
293
|
+
}
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Shared tmux helpers.
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
function isTmuxNotFoundError(error) {
|
|
298
|
+
// runCommand surfaces the child's stderr in error.message, so the "no
|
|
299
|
+
// server" / "missing session" / "can't find window" signatures are visible
|
|
300
|
+
// without a separate stderr probe.
|
|
301
|
+
const message = errorMessage(error);
|
|
302
|
+
return (message.includes("no server running") ||
|
|
303
|
+
message.includes("can't find session") ||
|
|
304
|
+
message.includes("can't find window"));
|
|
305
|
+
}
|
|
306
|
+
function isTmuxDuplicateSessionError(error) {
|
|
307
|
+
return errorMessage(error).includes("duplicate session");
|
|
308
|
+
}
|
|
309
|
+
// Runs a tmux kill-* command and maps the outcome: success closes, a
|
|
310
|
+
// not-found signature means it was already gone, and the shutdown signal
|
|
311
|
+
// rethrows so callers can abort.
|
|
312
|
+
async function killTmuxTarget(arguments_, signal) {
|
|
313
|
+
try {
|
|
314
|
+
await runWorkspaceCommand("tmux", arguments_, signal);
|
|
315
|
+
return { kind: "closed" };
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (isSignalAborted(signal)) {
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
if (isTmuxNotFoundError(error)) {
|
|
322
|
+
return { kind: "missing" };
|
|
323
|
+
}
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function probeTmuxCommand(arguments_, signal) {
|
|
328
|
+
try {
|
|
329
|
+
return { status: "ok", output: await runWorkspaceCommand("tmux", arguments_, signal) };
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (isSignalAborted(signal)) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
if (isTmuxNotFoundError(error)) {
|
|
336
|
+
return { status: "missing" };
|
|
337
|
+
}
|
|
338
|
+
return { status: "failed", reason: errorMessage(error) };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAE1E,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;AAsFD,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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* the `workspaces` API.
|
|
8
8
|
*/
|
|
9
9
|
import { detectHostCapabilities } from "./host.js";
|
|
10
|
+
import { readEnvironmentVariable, writeError } from "./util.js";
|
|
10
11
|
import { isSignalAborted, } from "./workspaceAdapter.js";
|
|
11
12
|
export function resolveWorkspaceKind(arguments_) {
|
|
12
13
|
const { config, host } = arguments_;
|
|
@@ -36,20 +37,33 @@ const HOST_CAPABILITY_BY_KIND = {
|
|
|
36
37
|
tmux: "hasTmux",
|
|
37
38
|
zellij: "hasZellij",
|
|
38
39
|
};
|
|
40
|
+
const TMUX_SESSION_PER_TASK_ENV = "GROUNDCREW_TMUX_SESSION_PER_TASK";
|
|
41
|
+
const TMUX_SESSION_DEFAULT_WARNING = [
|
|
42
|
+
"WARNING: tmux window mode is deprecated; tmux session-per-task mode will become the default soon.",
|
|
43
|
+
`Opt in now with ${TMUX_SESSION_PER_TASK_ENV}=1.`,
|
|
44
|
+
"Migration: attach with `tmux attach -t <task>` instead of `tmux attach -t groundcrew:<task>`; each task gets its own tmux session; `crew stop` and `crew cleanup` close that managed session; set GROUNDCREW_KEEP_DEAD_WINDOWS=1 to keep exited scrollback.",
|
|
45
|
+
].join("\n");
|
|
39
46
|
const ADAPTER_LOADER_BY_KIND = {
|
|
40
47
|
cmux: async () => {
|
|
41
48
|
const { cmuxAdapter } = await import("./cmuxAdapter.js");
|
|
42
49
|
return cmuxAdapter;
|
|
43
50
|
},
|
|
44
51
|
tmux: async () => {
|
|
45
|
-
const {
|
|
46
|
-
|
|
52
|
+
const { createTmuxAdapter } = await import("./tmuxAdapter.js");
|
|
53
|
+
const sessionPerTask = resolveTmuxSessionPerTask();
|
|
54
|
+
if (!sessionPerTask) {
|
|
55
|
+
writeError(TMUX_SESSION_DEFAULT_WARNING);
|
|
56
|
+
}
|
|
57
|
+
return createTmuxAdapter({ sessionPerTask });
|
|
47
58
|
},
|
|
48
59
|
zellij: async () => {
|
|
49
60
|
const { zellijAdapter } = await import("./zellijAdapter.js");
|
|
50
61
|
return zellijAdapter;
|
|
51
62
|
},
|
|
52
63
|
};
|
|
64
|
+
function resolveTmuxSessionPerTask() {
|
|
65
|
+
return readEnvironmentVariable(TMUX_SESSION_PER_TASK_ENV) === "1";
|
|
66
|
+
}
|
|
53
67
|
function failIfBinaryUnavailable(kind, host) {
|
|
54
68
|
if (!host[HOST_CAPABILITY_BY_KIND[kind]]) {
|
|
55
69
|
throw new Error(`workspaceKind '${kind}' is set but the ${kind} binary is not on PATH. Install ${kind} or change the setting.`);
|
package/docs/troubleshooting.md
CHANGED
|
@@ -27,6 +27,21 @@ When a wrapped agent command fails, the tmux window closes immediately and the e
|
|
|
27
27
|
|
|
28
28
|
This applies to the tmux backend only.
|
|
29
29
|
|
|
30
|
+
## Tmux Workspaces Share One Session By Default
|
|
31
|
+
|
|
32
|
+
By default the tmux backend runs every task as a window inside one shared `groundcrew` session, so opening your own extra window or split while attached lands it next to every other task. This window mode is deprecated; when `crew` starts on the tmux backend without the new mode enabled, it warns that session-per-task mode will become the default soon.
|
|
33
|
+
|
|
34
|
+
To opt in before the default changes, set `GROUNDCREW_TMUX_SESSION_PER_TASK=1` in the env you launch `crew` from. Each task gets its own dedicated tmux session named after the task id (cmux-style), tagged with the `@groundcrew_managed` tmux option.
|
|
35
|
+
|
|
36
|
+
Migration plan:
|
|
37
|
+
|
|
38
|
+
- Attach with `tmux attach -t <task>` instead of `tmux attach -t groundcrew:<task>`.
|
|
39
|
+
- Treat each task as a full tmux session: windows are task-local tabs and panes are task-local splits.
|
|
40
|
+
- `crew stop` and `crew cleanup` close the whole managed task session, including extra windows and panes you opened inside it. Same-named user sessions without `@groundcrew_managed` are ignored.
|
|
41
|
+
- Finished task sessions disappear once the command exits unless `GROUNDCREW_KEEP_DEAD_WINDOWS=1` is set; with that env, `crew status` reports the kept session as `exited` for scrollback inspection.
|
|
42
|
+
|
|
43
|
+
This applies to the tmux backend only.
|
|
44
|
+
|
|
30
45
|
## Tasks Stay In-Progress
|
|
31
46
|
|
|
32
47
|
Groundcrew marks a task `In Progress` when it provisions a workspace. When a PR opens on that worktree branch, the reviewer pass attempts to mark the task `In Review`. Linear's default `In Review` status works out of the box; if your team renamed it, configure `sources: [{ kind: "linear", statuses: { inReview: ["Code Review"] } }]`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.37.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",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"verify": "node scripts/verifyAll.mts"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@anthropic-ai/sandbox-runtime": "0.0.
|
|
71
|
+
"@anthropic-ai/sandbox-runtime": "0.0.54",
|
|
72
72
|
"@clipboard-health/clearance": "1.3.2",
|
|
73
73
|
"@linear/sdk": "86.0.0",
|
|
74
74
|
"cosmiconfig": "9.0.1",
|