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