@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.
Files changed (33) hide show
  1. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  2. package/dist/commands/resumeWorkspace.js +9 -0
  3. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  4. package/dist/commands/setupWorkspace.js +9 -0
  5. package/dist/lib/adapters/todo-txt/fileErrors.d.ts +4 -0
  6. package/dist/lib/adapters/todo-txt/fileErrors.d.ts.map +1 -0
  7. package/dist/lib/adapters/todo-txt/fileErrors.js +20 -0
  8. package/dist/lib/adapters/todo-txt/source.d.ts.map +1 -1
  9. package/dist/lib/adapters/todo-txt/source.js +16 -5
  10. package/dist/lib/adapters/todo-txt/writeback.d.ts.map +1 -1
  11. package/dist/lib/adapters/todo-txt/writeback.js +10 -2
  12. package/dist/lib/agentLaunch.d.ts +1 -0
  13. package/dist/lib/agentLaunch.d.ts.map +1 -1
  14. package/dist/lib/agentLaunch.js +2 -0
  15. package/dist/lib/launchCommand.d.ts +5 -0
  16. package/dist/lib/launchCommand.d.ts.map +1 -1
  17. package/dist/lib/launchCommand.js +11 -2
  18. package/dist/lib/srtLaunch.d.ts +2 -0
  19. package/dist/lib/srtLaunch.d.ts.map +1 -1
  20. package/dist/lib/srtLaunch.js +1 -0
  21. package/dist/lib/srtPolicy.d.ts +5 -0
  22. package/dist/lib/srtPolicy.d.ts.map +1 -1
  23. package/dist/lib/srtPolicy.js +2 -0
  24. package/dist/lib/taskSourceFilesystem.d.ts +7 -0
  25. package/dist/lib/taskSourceFilesystem.d.ts.map +1 -0
  26. package/dist/lib/taskSourceFilesystem.js +39 -0
  27. package/dist/lib/tmuxAdapter.d.ts +22 -5
  28. package/dist/lib/tmuxAdapter.d.ts.map +1 -1
  29. package/dist/lib/tmuxAdapter.js +269 -89
  30. package/dist/lib/workspaces.d.ts.map +1 -1
  31. package/dist/lib/workspaces.js +16 -2
  32. package/docs/troubleshooting.md +15 -0
  33. 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;AAenE,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,CA8Ef;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
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;AAuBnE,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,CA+Hf;AAgJD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA6Cf"}
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,4 @@
1
+ export declare function fileErrorCode(error: unknown): string | undefined;
2
+ export declare function isFileErrorCode(error: unknown, code: string): boolean;
3
+ export declare function describeFileError(error: unknown): string;
4
+ //# sourceMappingURL=fileErrors.d.ts.map
@@ -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;AAK7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA8VxD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACtB,UAAU,CA2KZ"}
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 else @preserve -- no explicit else branch; full-suite V8 coverage remaps the synthetic else inconsistently. */
259
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
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":"AAYA,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;AA8GD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAChC,MAAM,EAAE,CA0CV"}
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 [`missing todo file: ${todoPath}`];
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;CACnD,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAgChE;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"}
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"}
@@ -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 safehouseAddDirsFlag = safehousePathListFlag("--add-dirs", arguments_.safehouseAddDirs ?? []);
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} ${safehouseAddDirsFlag}${safehouseAgentAddDirsReadOnlyFlag}${agentEnvPassFlag}"$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh "$_p"; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"; }`);
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
@@ -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,CA6ClB"}
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"}
@@ -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");
@@ -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,CAuHnF"}
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"}
@@ -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. Workspaces live as windows inside one dedicated
3
- * `groundcrew` tmux session; the window name is the task id. tmux can't
4
- * paint status pills, so `open` silently drops `spec.status`. This is the
5
- * Linux/WSL path where cmux is unavailable.
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
- export declare const tmuxAdapter: Adapter;
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;;;;;GAKG;AAEH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAY/B,eAAO,MAAM,WAAW,EAAE,OA+DzB,CAAC"}
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"}
@@ -1,8 +1,18 @@
1
1
  /**
2
- * tmux Workspace backend. Workspaces live as windows inside one dedicated
3
- * `groundcrew` tmux session; the window name is the task id. tmux can't
4
- * paint status pills, so `open` silently drops `spec.status`. This is the
5
- * Linux/WSL path where cmux is unavailable.
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
- export const tmuxAdapter = {
17
- async open(spec, signal) {
18
- await ensureTmuxSession(signal);
19
- const target = tmuxTarget(spec.name);
20
- const keepDeadWindows = shouldKeepDeadWindows();
21
- await runWorkspaceCommand("tmux", [
22
- "new-window",
23
- "-d",
24
- "-t",
25
- TMUX_SESSION,
26
- "-n",
27
- spec.name,
28
- "-c",
29
- spec.cwd,
30
- spec.command,
31
- ";",
32
- "set-window-option",
33
- "-t",
34
- target,
35
- "remain-on-exit",
36
- keepDeadWindows ? "on" : "off",
37
- ";",
38
- "set-window-option",
39
- "-t",
40
- target,
41
- "allow-rename",
42
- "off",
43
- ], signal);
44
- // tmux can't paint status pills; spec.status is silently dropped.
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
- function isTmuxNotFoundError(error) {
85
- // runCommand surfaces the child's stderr in error.message, so the "no
86
- // server" / "missing session" / "can't find window" signatures are visible
87
- // without a separate stderr probe.
88
- const message = errorMessage(error);
89
- return (message.includes("no server running") ||
90
- message.includes("can't find session") ||
91
- message.includes("can't find window"));
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 probeTmuxList(format, signal) {
94
- try {
95
- return {
96
- status: "ok",
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
- catch (error) {
101
- if (isSignalAborted(signal)) {
102
- throw error;
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;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"}
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"}
@@ -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 { tmuxAdapter } = await import("./tmuxAdapter.js");
46
- return tmuxAdapter;
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.`);
@@ -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.36.1",
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.52",
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",