@clipboard-health/groundcrew 4.36.2 → 4.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/commands/dispatcher.d.ts.map +1 -1
  2. package/dist/commands/dispatcher.js +7 -0
  3. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  4. package/dist/commands/resumeWorkspace.js +27 -4
  5. package/dist/commands/setupWorkspace.d.ts +2 -0
  6. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  7. package/dist/commands/setupWorkspace.js +21 -2
  8. package/dist/lib/adapters/todo-txt/fileErrors.d.ts +4 -0
  9. package/dist/lib/adapters/todo-txt/fileErrors.d.ts.map +1 -0
  10. package/dist/lib/adapters/todo-txt/fileErrors.js +20 -0
  11. package/dist/lib/adapters/todo-txt/source.d.ts.map +1 -1
  12. package/dist/lib/adapters/todo-txt/source.js +16 -5
  13. package/dist/lib/adapters/todo-txt/writeback.d.ts.map +1 -1
  14. package/dist/lib/adapters/todo-txt/writeback.js +10 -2
  15. package/dist/lib/agentLaunch.d.ts +1 -0
  16. package/dist/lib/agentLaunch.d.ts.map +1 -1
  17. package/dist/lib/agentLaunch.js +2 -0
  18. package/dist/lib/config.js +1 -1
  19. package/dist/lib/launchCommand.d.ts +13 -4
  20. package/dist/lib/launchCommand.d.ts.map +1 -1
  21. package/dist/lib/launchCommand.js +26 -6
  22. package/dist/lib/sourceCapabilities.d.ts +8 -0
  23. package/dist/lib/sourceCapabilities.d.ts.map +1 -1
  24. package/dist/lib/sourceCapabilities.js +26 -1
  25. package/dist/lib/srtLaunch.d.ts +2 -0
  26. package/dist/lib/srtLaunch.d.ts.map +1 -1
  27. package/dist/lib/srtLaunch.js +1 -0
  28. package/dist/lib/srtPolicy.d.ts +5 -0
  29. package/dist/lib/srtPolicy.d.ts.map +1 -1
  30. package/dist/lib/srtPolicy.js +2 -0
  31. package/dist/lib/taskSourceFilesystem.d.ts +7 -0
  32. package/dist/lib/taskSourceFilesystem.d.ts.map +1 -0
  33. package/dist/lib/taskSourceFilesystem.js +39 -0
  34. package/dist/lib/tmuxAdapter.d.ts +22 -5
  35. package/dist/lib/tmuxAdapter.d.ts.map +1 -1
  36. package/dist/lib/tmuxAdapter.js +269 -89
  37. package/dist/lib/workspaces.d.ts.map +1 -1
  38. package/dist/lib/workspaces.js +16 -2
  39. package/docs/task-sources.md +5 -4
  40. package/docs/troubleshooting.md +15 -0
  41. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,UAAU,EAAE;QACpB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAiCD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAoOjE;AA2BD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAQrE"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,UAAU,EAAE;QACpB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAiCD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAyOjE;AA2BD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAQrE"}
@@ -6,7 +6,9 @@
6
6
  * Pure verdict logic lives in `eligibility.ts`; this module is responsible
7
7
  * for telemetry, writeback via Board, and side-effecting setupWorkspace calls.
8
8
  */
9
+ import { sourcesFromConfig } from "../lib/buildSources.js";
9
10
  import { dispatchableRepository, formatKnownRepositories } from "../lib/repositoryValidation.js";
11
+ import { sourceSupportsMarkDone } from "../lib/sourceCapabilities.js";
10
12
  import { isGroundcrewIssue, naturalIdFromCanonical, } from "../lib/taskSource.js";
11
13
  import { errorMessage, failMark, log, logEvent, styleWarning } from "../lib/util.js";
12
14
  import { workspaces } from "../lib/workspaces.js";
@@ -35,6 +37,7 @@ function logMissingRepositorySkip(issue, agent, knownRepositories) {
35
37
  }
36
38
  export function createDispatcher(deps) {
37
39
  const { config, board } = deps;
40
+ const rawSources = sourcesFromConfig(config);
38
41
  function buildExhaustedSet(usage) {
39
42
  const exhausted = new Set();
40
43
  for (const exhaustion of classifyUsageExhaustion(config, usage)) {
@@ -71,6 +74,10 @@ export function createDispatcher(deps) {
71
74
  repository: issue.repository,
72
75
  task: taskId,
73
76
  completionTaskId: issue.id,
77
+ completionMarkDoneSupported: sourceSupportsMarkDone({
78
+ rawSources,
79
+ sourceName: issue.source,
80
+ }),
74
81
  agent: issue.agent,
75
82
  details: {
76
83
  title: issue.title,
@@ -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;AAiBnE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAqJD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA0Ff;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
@@ -1,11 +1,13 @@
1
1
  import { fetchResolvedIssue } from "../lib/adapters/linear/fetch.js";
2
2
  import { getLinearClient } from "../lib/adapters/linear/client.js";
3
- import { isLinearEnabled } from "../lib/buildSources.js";
3
+ import { isLinearEnabled, sourcesFromConfig } from "../lib/buildSources.js";
4
4
  import { loadConfig } from "../lib/config.js";
5
5
  import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
6
6
  import { workerEnvironmentForTask } from "../lib/launchCommand.js";
7
7
  import { readRunState, recordRunState } from "../lib/runState.js";
8
+ import { taskSupportsCompletionCommand } from "../lib/sourceCapabilities.js";
8
9
  import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
10
+ import { taskSourceWritePathsForCompletion } from "../lib/taskSourceFilesystem.js";
9
11
  import { naturalIdFromCanonical, toCanonicalId } from "../lib/taskSource.js";
10
12
  import { errorMessage, log } from "../lib/util.js";
11
13
  import { workspaces } from "../lib/workspaces.js";
@@ -32,6 +34,7 @@ async function fetchTaskDetails(task) {
32
34
  }
33
35
  async function contextFromLinear(config, task, worktree) {
34
36
  const resolved = await fetchResolvedIssue({ client: getLinearClient(), config, task });
37
+ const completionTaskId = toCanonicalId("linear", task);
35
38
  return {
36
39
  task,
37
40
  repository: resolved.repository,
@@ -39,7 +42,11 @@ async function contextFromLinear(config, task, worktree) {
39
42
  worktree,
40
43
  title: resolved.title,
41
44
  description: resolved.description,
42
- completionTaskId: toCanonicalId("linear", task),
45
+ completionTaskId,
46
+ completionMarkDoneSupported: taskSupportsCompletionCommand({
47
+ rawSources: sourcesFromConfig(config),
48
+ taskId: completionTaskId,
49
+ }),
43
50
  resumeCount: 0,
44
51
  };
45
52
  }
@@ -48,6 +55,7 @@ async function contextFromState(config, task, state, worktree) {
48
55
  // missing-API-key error logs noisily even though resume only needs it to
49
56
  // enrich the prompt title/description (which falls back to the task id).
50
57
  const details = isLinearEnabled(config) ? await fetchTaskDetails(task) : undefined;
58
+ const completionTaskId = state.completionTaskId ?? task;
51
59
  return {
52
60
  task,
53
61
  repository: state.repository,
@@ -55,7 +63,11 @@ async function contextFromState(config, task, state, worktree) {
55
63
  worktree,
56
64
  title: details?.title ?? task.toUpperCase(),
57
65
  description: details?.description ?? "",
58
- completionTaskId: state.completionTaskId ?? task,
66
+ completionTaskId,
67
+ completionMarkDoneSupported: taskSupportsCompletionCommand({
68
+ rawSources: sourcesFromConfig(config),
69
+ taskId: completionTaskId,
70
+ }),
59
71
  ...(state.reason === undefined ? {} : { reason: state.reason }),
60
72
  resumeCount: state.resumeCount,
61
73
  };
@@ -141,6 +153,13 @@ export async function resumeWorkspace(config, options) {
141
153
  let srtSettingsDir;
142
154
  try {
143
155
  let launchCommand;
156
+ const taskSourceWritePaths = runner === "safehouse" || runner === "srt"
157
+ ? taskSourceWritePathsForCompletion({
158
+ config,
159
+ taskId: context.completionTaskId,
160
+ workingDir: launchDir,
161
+ })
162
+ : undefined;
144
163
  ({ launchCommand, srtSettingsDir } = composeAgentLaunch({
145
164
  runner,
146
165
  task,
@@ -151,7 +170,11 @@ export async function resumeWorkspace(config, options) {
151
170
  secretsFile,
152
171
  sandboxName,
153
172
  workspaceKind,
154
- workerEnvironment: workerEnvironmentForTask(context.completionTaskId),
173
+ workerEnvironment: workerEnvironmentForTask({
174
+ taskId: context.completionTaskId,
175
+ markDoneSupported: context.completionMarkDoneSupported,
176
+ }),
177
+ taskSourceWritePaths,
155
178
  }));
156
179
  const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
157
180
  await openAgentWorkspace({
@@ -9,6 +9,8 @@ export interface SetupWorkspaceOptions {
9
9
  task: string;
10
10
  /** Canonical source id for worker self-completion; falls back to `task`. */
11
11
  completionTaskId?: string;
12
+ /** Whether the task source can apply `crew task done`; defaults to true for direct calls. */
13
+ completionMarkDoneSupported?: boolean;
12
14
  repository: string;
13
15
  agent: string;
14
16
  details: TaskDetails;
@@ -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;AAyBnE,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,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,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,CA4If;AAgJD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -6,7 +6,9 @@ import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
6
6
  import { workerEnvironmentForTask } from "../lib/launchCommand.js";
7
7
  import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
8
8
  import { recordRunState } from "../lib/runState.js";
9
+ import { sourceSupportsMarkDone } from "../lib/sourceCapabilities.js";
9
10
  import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
11
+ import { taskSourceWritePathsForCompletion } from "../lib/taskSourceFilesystem.js";
10
12
  import { naturalIdFromCanonical } from "../lib/taskSource.js";
11
13
  import { debug, errorMessage, log, okMark } from "../lib/util.js";
12
14
  import { workspaces } from "../lib/workspaces.js";
@@ -82,6 +84,14 @@ export async function setupWorkspace(config, options, runOptions = {}) {
82
84
  });
83
85
  const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(promptDir);
84
86
  const completionTaskId = options.completionTaskId ?? task;
87
+ const completionMarkDoneSupported = options.completionMarkDoneSupported ?? true;
88
+ const taskSourceWritePaths = runner === "safehouse" || runner === "srt"
89
+ ? taskSourceWritePathsForCompletion({
90
+ config,
91
+ taskId: completionTaskId,
92
+ workingDir: launchDir,
93
+ })
94
+ : undefined;
85
95
  const { launchCommand, srtSettingsDir: stagedSrtSettingsDir } = composeAgentLaunch({
86
96
  runner,
87
97
  task,
@@ -93,7 +103,11 @@ export async function setupWorkspace(config, options, runOptions = {}) {
93
103
  prepareWorktreeCommand,
94
104
  sandboxName,
95
105
  workspaceKind,
96
- workerEnvironment: workerEnvironmentForTask(completionTaskId),
106
+ workerEnvironment: workerEnvironmentForTask({
107
+ taskId: completionTaskId,
108
+ markDoneSupported: completionMarkDoneSupported,
109
+ }),
110
+ taskSourceWritePaths,
97
111
  });
98
112
  srtSettingsDir = stagedSrtSettingsDir;
99
113
  const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
@@ -255,9 +269,10 @@ async function rollbackWorktree(arguments_) {
255
269
  }
256
270
  export async function setupWorkspaceCli(task, options = {}) {
257
271
  const config = await loadConfig();
272
+ const rawSources = sourcesFromConfig(config);
258
273
  let sources;
259
274
  try {
260
- sources = await buildSources(sourcesFromConfig(config), { globalConfig: config });
275
+ sources = await buildSources(rawSources, { globalConfig: config });
261
276
  }
262
277
  catch (error) {
263
278
  /* v8 ignore next @preserve -- catch re-throw always receives an Error; String(error) is an unreachable fallback */
@@ -283,6 +298,10 @@ export async function setupWorkspaceCli(task, options = {}) {
283
298
  await setupWorkspace(config, {
284
299
  task: naturalId,
285
300
  completionTaskId: resolved.id,
301
+ completionMarkDoneSupported: sourceSupportsMarkDone({
302
+ rawSources,
303
+ sourceName: resolved.source,
304
+ }),
286
305
  repository: resolved.repository,
287
306
  agent: resolved.agent,
288
307
  details: {
@@ -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 };
@@ -108,7 +108,7 @@ const DEFAULT_PROMPT_INITIAL = [
108
108
  "2. Implement the smallest sensible change that completes the task.",
109
109
  "3. Run the repo's documented verification command. If no documented command exists, run the smallest relevant test suite you can find and fix failures you introduced before continuing.",
110
110
  "4. Follow the task description for output. If no output instructions exist, open a PR with `Closes {{task}}` in the description. If you cannot open one, leave the branch ready and record the blocker.",
111
- "5. If the requested work is complete, no PR is needed, and any dirty worktree state is expected or explicitly allowed, run the command in `GROUNDCREW_COMPLETE` to mark the task done.",
111
+ "5. If the requested work is complete, no PR is needed, `GROUNDCREW_COMPLETE` is set, and any dirty worktree state is expected or explicitly allowed, run the command in `GROUNDCREW_COMPLETE` to mark the task done.",
112
112
  ].join("\n");
113
113
  const ALLOWED_PROMPT_PLACEHOLDERS = new Set([
114
114
  "{{task}}",
@@ -37,10 +37,14 @@ export declare function isEnvironmentAssignment(token: string): boolean;
37
37
  * profile; srt uses it to pick the agent's credential profile in `srtPolicy`.
38
38
  */
39
39
  export declare function inferAgentCommandName(agentCmd: string): string;
40
- declare const WORKER_ENVIRONMENT_NAMES: readonly ["GROUNDCREW_TASK_ID", "GROUNDCREW_COMPLETE"];
41
- type WorkerEnvironmentName = (typeof WORKER_ENVIRONMENT_NAMES)[number];
42
- export type WorkerEnvironment = Readonly<Record<WorkerEnvironmentName, string>>;
43
- export declare function workerEnvironmentForTask(taskId: string): WorkerEnvironment;
40
+ export type WorkerEnvironment = Readonly<{
41
+ GROUNDCREW_TASK_ID: string;
42
+ GROUNDCREW_COMPLETE?: string;
43
+ }>;
44
+ export declare function workerEnvironmentForTask(arguments_: {
45
+ taskId: string;
46
+ markDoneSupported: boolean;
47
+ }): WorkerEnvironment;
44
48
  export interface SafehouseAgentIntegration {
45
49
  addDirsReadOnly: readonly string[];
46
50
  envPass: readonly string[];
@@ -120,6 +124,11 @@ interface LaunchCommandArguments {
120
124
  * pre-existing behavior). Only consumed by the safehouse wrap.
121
125
  */
122
126
  safehouseAddDirs?: readonly string[] | undefined;
127
+ /**
128
+ * Extra read/write paths granted only to the Safehouse agent wrap. These are
129
+ * intentionally withheld from the repo-controlled prepareWorktree wrap.
130
+ */
131
+ safehouseAgentAddDirs?: readonly string[] | undefined;
123
132
  /**
124
133
  * Extra host-terminal integration surface granted only to the Safehouse agent
125
134
  * 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;AAMD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC,CAAC;AAEH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,OAAO,CAAC;CAC5B,GAAG,iBAAiB,CAMpB;AA8BD,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"}
@@ -248,20 +248,31 @@ export function inferAgentCommandName(agentCmd) {
248
248
  return commandName;
249
249
  }
250
250
  const WORKER_ENVIRONMENT_NAMES = ["GROUNDCREW_TASK_ID", "GROUNDCREW_COMPLETE"];
251
- export function workerEnvironmentForTask(taskId) {
251
+ export function workerEnvironmentForTask(arguments_) {
252
+ const { taskId, markDoneSupported } = arguments_;
252
253
  return {
253
254
  GROUNDCREW_TASK_ID: taskId,
254
- GROUNDCREW_COMPLETE: `crew task done ${taskId}`,
255
+ ...(markDoneSupported ? { GROUNDCREW_COMPLETE: `crew task done ${taskId}` } : {}),
255
256
  };
256
257
  }
257
258
  function workerEnvironmentNames(workerEnvironment) {
258
- return workerEnvironment === undefined ? [] : WORKER_ENVIRONMENT_NAMES;
259
+ if (workerEnvironment === undefined) {
260
+ return [];
261
+ }
262
+ return WORKER_ENVIRONMENT_NAMES.filter((name) => workerEnvironment[name] !== undefined);
259
263
  }
260
264
  function workerEnvironmentExports(workerEnvironment) {
261
265
  if (workerEnvironment === undefined) {
262
266
  return [];
263
267
  }
264
- return WORKER_ENVIRONMENT_NAMES.map((name) => `export ${name}=${shellSingleQuote(workerEnvironment[name])}`);
268
+ const exports = [];
269
+ for (const name of WORKER_ENVIRONMENT_NAMES) {
270
+ const value = workerEnvironment[name];
271
+ if (value !== undefined) {
272
+ exports.push(`export ${name}=${shellSingleQuote(value)}`);
273
+ }
274
+ }
275
+ return exports;
265
276
  }
266
277
  function envPassFlag(names) {
267
278
  const uniqueNames = [...new Set(names)];
@@ -404,7 +415,13 @@ function buildSafehouseLaunchCommand(arguments_) {
404
415
  // Quote the whole value so shell-special chars survive; the trailing space
405
416
  // separates it from the next argv token. See `resolveSafehouseAddDirs` for
406
417
  // which paths these are and why.
407
- const safehouseAddDirsFlag = safehousePathListFlag("--add-dirs", arguments_.safehouseAddDirs ?? []);
418
+ const safehousePrepareAddDirs = arguments_.safehouseAddDirs ?? [];
419
+ const safehouseAgentAddDirs = uniqueStrings([
420
+ ...safehousePrepareAddDirs,
421
+ ...(arguments_.safehouseAgentAddDirs ?? []),
422
+ ]);
423
+ const safehouseAddDirsFlag = safehousePathListFlag("--add-dirs", safehousePrepareAddDirs);
424
+ const safehouseAgentAddDirsFlag = safehousePathListFlag("--add-dirs", safehouseAgentAddDirs);
408
425
  const safehouseAgentAddDirsReadOnlyFlag = safehousePathListFlag("--add-dirs-ro", safehouseAgentIntegration?.addDirsReadOnly ?? []);
409
426
  const safehouseWrapper = safehouseClearanceWrapperCommand();
410
427
  // Defensive shim+promptDir trap: by the time we arm it, `rm -rf <promptDir>`
@@ -435,12 +452,15 @@ function buildSafehouseLaunchCommand(arguments_) {
435
452
  // Running the real launch chain as `sh -c` would make it see `sh`, so use
436
453
  // an agent-named symlink to /bin/sh. This preserves per-agent profile
437
454
  // 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"; }`);
455
+ `{ ${safehouseWrapper} ${safehouseAgentAddDirsFlag}${safehouseAgentAddDirsReadOnlyFlag}${agentEnvPassFlag}"$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh "$_p"; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"; }`);
439
456
  return lines.join(" && ");
440
457
  }
441
458
  function safehousePathListFlag(flagName, paths) {
442
459
  return paths.length === 0 ? "" : `${flagName}=${shellSingleQuote(paths.join(":"))} `;
443
460
  }
461
+ function uniqueStrings(values) {
462
+ return [...new Set(values)];
463
+ }
444
464
  /**
445
465
  * Benign baseline env the srt wraps run under (via `env -i`). This is an
446
466
  * allowlist on purpose: srt's CLI spawns its child with the *inherited* host
@@ -14,5 +14,13 @@ export interface SourceSummary {
14
14
  capabilities: SourceCapabilities;
15
15
  }
16
16
  export declare function summarizeSource(raw: unknown): SourceSummary;
17
+ export declare function sourceSupportsMarkDone(arguments_: {
18
+ rawSources: readonly unknown[];
19
+ sourceName: string;
20
+ }): boolean;
21
+ export declare function taskSupportsCompletionCommand(arguments_: {
22
+ rawSources: readonly unknown[];
23
+ taskId: string;
24
+ }): boolean;
17
25
  export {};
18
26
  //# sourceMappingURL=sourceCapabilities.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sourceCapabilities.d.ts","sourceRoot":"","sources":["../../src/lib/sourceCapabilities.ts"],"names":[],"mappings":"AAKA,UAAU,kBAAkB;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,kBAAkB,CAAC;CAClC;AAoDD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAiB3D"}
1
+ {"version":3,"file":"sourceCapabilities.d.ts","sourceRoot":"","sources":["../../src/lib/sourceCapabilities.ts"],"names":[],"mappings":"AAIA,UAAU,kBAAkB;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,kBAAkB,CAAC;CAClC;AAqDD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAiB3D;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE;IACjD,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CASV;AAED,wBAAgB,6BAA6B,CAAC,UAAU,EAAE;IACxD,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAcV"}
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { shellAdapterConfigSchema } from "./adapters/shell/schema.js";
3
- import { kindShape } from "./buildSources.js";
4
3
  const nameShape = z.looseObject({ name: z.string().optional() });
4
+ const kindShape = z.object({ kind: z.string() });
5
5
  const LINEAR_CAPABILITIES = {
6
6
  verify: true,
7
7
  listTasks: true,
@@ -65,3 +65,28 @@ export function summarizeSource(raw) {
65
65
  }
66
66
  return { name: sourceName, kind, capabilities };
67
67
  }
68
+ export function sourceSupportsMarkDone(arguments_) {
69
+ const { rawSources, sourceName } = arguments_;
70
+ for (const rawSource of rawSources) {
71
+ const source = summarizeSource(rawSource);
72
+ if (source.name === sourceName) {
73
+ return source.capabilities.markDone;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+ export function taskSupportsCompletionCommand(arguments_) {
79
+ const { rawSources, taskId } = arguments_;
80
+ const colonIndex = taskId.indexOf(":");
81
+ if (colonIndex === -1) {
82
+ const [singleSource] = rawSources;
83
+ if (rawSources.length === 1 && singleSource !== undefined) {
84
+ return summarizeSource(singleSource).capabilities.markDone;
85
+ }
86
+ return false;
87
+ }
88
+ return sourceSupportsMarkDone({
89
+ rawSources,
90
+ sourceName: taskId.slice(0, colonIndex),
91
+ });
92
+ }
@@ -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.`);
@@ -40,10 +40,11 @@ completed no-PR work. If omitted, groundcrew treats done advancement as
40
40
  unsupported and leaves the task for the source's own integration to close out.
41
41
  `${id}`, `${canonicalId}`, and `${name}` placeholders are shell-quoted before substitution.
42
42
 
43
- Workers receive `GROUNDCREW_TASK_ID` and `GROUNDCREW_COMPLETE` in their launch
44
- environment. The default prompt tells them to run `GROUNDCREW_COMPLETE` only
45
- when the requested work is complete, no PR is needed, and any dirty worktree
46
- state is expected or explicitly allowed.
43
+ Workers receive `GROUNDCREW_TASK_ID` in their launch environment. They receive
44
+ `GROUNDCREW_COMPLETE` only when the source supports done writeback. The default
45
+ prompt tells them to run `GROUNDCREW_COMPLETE` only when it is set, the
46
+ requested work is complete, no PR is needed, and any dirty worktree state is
47
+ expected or explicitly allowed.
47
48
 
48
49
  ```json
49
50
  [
@@ -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.2",
3
+ "version": "4.37.1",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",