@clipboard-health/groundcrew 4.30.0 → 4.31.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/README.md +3 -3
- package/crew.config.example.ts +23 -0
- package/dist/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +20 -36
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +12 -30
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/agentLaunch.d.ts +21 -0
- package/dist/lib/agentLaunch.d.ts.map +1 -1
- package/dist/lib/agentLaunch.js +37 -0
- package/dist/lib/clearanceAllowlist.d.ts +8 -0
- package/dist/lib/clearanceAllowlist.d.ts.map +1 -0
- package/dist/lib/clearanceAllowlist.js +26 -0
- package/dist/lib/clearanceHosts.js +1 -10
- package/dist/lib/config.d.ts +32 -2
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +52 -25
- package/dist/lib/launchCommand.d.ts +7 -0
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +14 -9
- package/dist/lib/pathList.d.ts +2 -0
- package/dist/lib/pathList.d.ts.map +1 -0
- package/dist/lib/pathList.js +11 -0
- package/dist/lib/srtLaunch.d.ts +1 -3
- package/dist/lib/srtLaunch.d.ts.map +1 -1
- package/dist/lib/srtLaunch.js +24 -4
- package/dist/lib/worktrees.d.ts +8 -0
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +149 -16
- package/docs/configuration.md +36 -0
- package/docs/runners.md +6 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,8 +50,8 @@ crew init --global --project-dir ~/dev --repo OWNER/REPO --agent claude
|
|
|
50
50
|
|
|
51
51
|
# 3. Run the clone commands printed by `crew init`.
|
|
52
52
|
|
|
53
|
-
# 4.
|
|
54
|
-
|
|
53
|
+
# 4. Safehouse runs use groundcrew's bundled clearance allowlist automatically.
|
|
54
|
+
# Add extra hosts later via CLEARANCE_ALLOW_HOSTS or CLEARANCE_ALLOW_HOSTS_FILES.
|
|
55
55
|
|
|
56
56
|
# 5. Using Linear? Export your API key. (Jira and other trackers: see Task Pickup.)
|
|
57
57
|
export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
|
|
@@ -145,7 +145,7 @@ There is no `linear` config block. Groundcrew reads `GROUNDCREW_LINEAR_API_KEY`
|
|
|
145
145
|
|
|
146
146
|
## Reference
|
|
147
147
|
|
|
148
|
-
- [Configuration](./docs/configuration.md): discovery order, repo layout, full config table, prompt customization.
|
|
148
|
+
- [Configuration](./docs/configuration.md): discovery order, repo layout, scripted/sparse-checkout (graft) worktrees, full config table, prompt customization.
|
|
149
149
|
- [Runners](./docs/runners.md): Safehouse, Docker Sandboxes, and the `none` escape hatch.
|
|
150
150
|
- [Credentials](./docs/credentials.md): Linear API keys, 1Password, build secrets, and `preLaunch`.
|
|
151
151
|
- [Prepare worktree hooks](./docs/setup-hooks.md): `.groundcrew/config.json` `hooks.prepareWorktree` for per-repo dependency setup.
|
package/crew.config.example.ts
CHANGED
|
@@ -26,6 +26,29 @@ export default {
|
|
|
26
26
|
// form to point a repo at a different parent directory:
|
|
27
27
|
// { name: "other-org/other-repo", projectDirOverride: "~/work" }
|
|
28
28
|
knownRepositories: ["your-org/your-repo"],
|
|
29
|
+
// A knownRepositories entry can also be an object that provisions the
|
|
30
|
+
// worktree with a custom command instead of `git worktree add` — e.g. a
|
|
31
|
+
// sparse checkout via `graft`. `repo` is a logical name (task token +
|
|
32
|
+
// worktree dir basename); the physical clone is the command's concern.
|
|
33
|
+
// Templates interpolate ${branch} ${dir} ${baseRef} ${repo} ${task}.
|
|
34
|
+
//
|
|
35
|
+
// {
|
|
36
|
+
// name: "billing",
|
|
37
|
+
// provision: {
|
|
38
|
+
// create: "graft new ${branch} billing --from ${baseRef} --dir ${dir}",
|
|
39
|
+
// remove: "graft rm ${branch} -f",
|
|
40
|
+
// },
|
|
41
|
+
// // Optional: run the agent, prepareWorktree hook, and
|
|
42
|
+
// // .groundcrew/config.json lookup in this subdirectory of the
|
|
43
|
+
// // checkout (relative, no ".."). Use it when the checkout is a
|
|
44
|
+
// // monorepo whose project lives in a subdir.
|
|
45
|
+
// workdir: "services/billing",
|
|
46
|
+
// },
|
|
47
|
+
//
|
|
48
|
+
// Set up graft once outside groundcrew:
|
|
49
|
+
// graft repo add ~/dev/owner/monorepo
|
|
50
|
+
// graft alias add billing services/billing libs/common
|
|
51
|
+
// `crew doctor` does not parse or validate these shell templates.
|
|
29
52
|
},
|
|
30
53
|
agents: {
|
|
31
54
|
default: "claude",
|
|
@@ -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;AAanE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAuID,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA2Ef;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
|
|
@@ -2,14 +2,12 @@ import { fetchResolvedIssue } from "../lib/adapters/linear/fetch.js";
|
|
|
2
2
|
import { getLinearClient } from "../lib/adapters/linear/client.js";
|
|
3
3
|
import { isLinearEnabled } from "../lib/buildSources.js";
|
|
4
4
|
import { loadConfig } from "../lib/config.js";
|
|
5
|
-
import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
6
|
-
import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
5
|
+
import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
7
6
|
import { readRunState, recordRunState } from "../lib/runState.js";
|
|
8
|
-
import { buildAndStageSrtLaunch } from "../lib/srtLaunch.js";
|
|
9
7
|
import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
10
8
|
import { errorMessage, log } from "../lib/util.js";
|
|
11
9
|
import { workspaces } from "../lib/workspaces.js";
|
|
12
|
-
import { worktrees } from "../lib/worktrees.js";
|
|
10
|
+
import { resolveLaunchDir, worktrees } from "../lib/worktrees.js";
|
|
13
11
|
function parseArguments(argv) {
|
|
14
12
|
const [task, ...extras] = argv;
|
|
15
13
|
if (task === undefined || task.length === 0 || extras.length > 0 || task.startsWith("-")) {
|
|
@@ -124,50 +122,36 @@ export async function resumeWorkspace(config, options) {
|
|
|
124
122
|
purpose: "resumes",
|
|
125
123
|
});
|
|
126
124
|
await ensureReady();
|
|
125
|
+
const worktreeDir = context.worktree.dir;
|
|
126
|
+
const launchDir = resolveLaunchDir(config, context.repository, worktreeDir);
|
|
127
127
|
const stagedPrompt = stagePromptText({
|
|
128
128
|
prefix: "groundcrew-resume",
|
|
129
129
|
task,
|
|
130
130
|
text: renderResumePrompt(context),
|
|
131
131
|
});
|
|
132
132
|
const secretsFile = stageBuildSecrets(stagedPrompt.directory);
|
|
133
|
-
// Resume
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
let srtAgentSettingsFile;
|
|
133
|
+
// Resume stages srt settings exactly like setup (a relocating agent such as
|
|
134
|
+
// codex needs its config home re-seeded to authenticate on the resumed launch).
|
|
135
|
+
// Composition runs inside the try so a pre-launch failure still cleans up the
|
|
136
|
+
// staged prompt (and any srt settings) dir.
|
|
138
137
|
let srtSettingsDir;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
repository: context.repository,
|
|
138
|
+
try {
|
|
139
|
+
let launchCommand;
|
|
140
|
+
({ launchCommand, srtSettingsDir } = composeAgentLaunch({
|
|
141
|
+
runner,
|
|
144
142
|
task,
|
|
145
|
-
worktreeDir: context.worktree.dir,
|
|
146
143
|
definition,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
definition,
|
|
155
|
-
promptFile: stagedPrompt.file,
|
|
156
|
-
worktreeDir: context.worktree.dir,
|
|
157
|
-
secretsFile,
|
|
158
|
-
runner,
|
|
159
|
-
sandboxName,
|
|
160
|
-
srtPrepareSettingsFile,
|
|
161
|
-
srtAgentSettingsFile,
|
|
162
|
-
srtSettingsDir,
|
|
163
|
-
srtAgentConfigDirEnv,
|
|
164
|
-
});
|
|
165
|
-
const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
|
|
166
|
-
try {
|
|
144
|
+
promptFile: stagedPrompt.file,
|
|
145
|
+
worktreeDir,
|
|
146
|
+
workingDir: launchDir,
|
|
147
|
+
secretsFile,
|
|
148
|
+
sandboxName,
|
|
149
|
+
}));
|
|
150
|
+
const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
|
|
167
151
|
await openAgentWorkspace({
|
|
168
152
|
config,
|
|
169
153
|
name: task,
|
|
170
|
-
cwd:
|
|
154
|
+
cwd: launchDir,
|
|
171
155
|
command: launchCmd,
|
|
172
156
|
agent: context.agent,
|
|
173
157
|
color: definition.color,
|
|
@@ -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;AAsBnE,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,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,CA0Hf;AA8ID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { rmSync } from "node:fs";
|
|
2
2
|
import { loadConfig } from "../lib/config.js";
|
|
3
|
-
import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
3
|
+
import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
4
4
|
import { createBoard } from "../lib/board.js";
|
|
5
5
|
import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
|
|
6
|
-
import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
7
6
|
import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
|
|
8
7
|
import { recordRunState } from "../lib/runState.js";
|
|
9
|
-
import { buildAndStageSrtLaunch } from "../lib/srtLaunch.js";
|
|
10
8
|
import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
11
9
|
import { naturalIdFromCanonical } from "../lib/taskSource.js";
|
|
12
10
|
import { debug, errorMessage, log, okMark } from "../lib/util.js";
|
|
13
11
|
import { workspaces } from "../lib/workspaces.js";
|
|
14
|
-
import { isWorktreeAlreadyExistsError, worktrees } from "../lib/worktrees.js";
|
|
12
|
+
import { isWorktreeAlreadyExistsError, resolveLaunchDir, worktrees, } from "../lib/worktrees.js";
|
|
15
13
|
function stagePrompt(input) {
|
|
16
14
|
return stagePromptFromTemplate({
|
|
17
15
|
config: input.config,
|
|
@@ -53,7 +51,8 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
53
51
|
}
|
|
54
52
|
throw error;
|
|
55
53
|
}
|
|
56
|
-
const { branchName, dir:
|
|
54
|
+
const { branchName, dir: worktreeDir } = created;
|
|
55
|
+
const launchDir = resolveLaunchDir(config, repository, worktreeDir);
|
|
57
56
|
const worktreeName = `${repository}-${task}`;
|
|
58
57
|
// Anything that fails after the worktree is on disk must roll it back
|
|
59
58
|
// (the worktree and the just-created branch). `workspaces.open` cleans
|
|
@@ -81,35 +80,18 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
81
80
|
defaultHooks: config.defaults.hooks,
|
|
82
81
|
});
|
|
83
82
|
const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(promptDir);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (runner === "srt") {
|
|
88
|
-
const staged = buildAndStageSrtLaunch({
|
|
89
|
-
config,
|
|
90
|
-
repository,
|
|
91
|
-
task,
|
|
92
|
-
worktreeDir: launchDir,
|
|
93
|
-
definition,
|
|
94
|
-
});
|
|
95
|
-
srtPrepareSettingsFile = staged.prepareFile;
|
|
96
|
-
srtAgentSettingsFile = staged.agentFile;
|
|
97
|
-
srtSettingsDir = staged.directory;
|
|
98
|
-
srtAgentConfigDirEnv = staged.agentConfigDirEnv;
|
|
99
|
-
}
|
|
100
|
-
const launchCommand = buildLaunchCommand({
|
|
83
|
+
const { launchCommand, srtSettingsDir: stagedSrtSettingsDir } = composeAgentLaunch({
|
|
84
|
+
runner,
|
|
85
|
+
task,
|
|
101
86
|
definition,
|
|
102
87
|
promptFile: stagedPrompt.file,
|
|
103
|
-
worktreeDir
|
|
88
|
+
worktreeDir,
|
|
89
|
+
workingDir: launchDir,
|
|
104
90
|
secretsFile,
|
|
105
91
|
prepareWorktreeCommand,
|
|
106
|
-
runner,
|
|
107
92
|
sandboxName,
|
|
108
|
-
srtPrepareSettingsFile,
|
|
109
|
-
srtAgentSettingsFile,
|
|
110
|
-
srtSettingsDir,
|
|
111
|
-
srtAgentConfigDirEnv,
|
|
112
93
|
});
|
|
94
|
+
srtSettingsDir = stagedSrtSettingsDir;
|
|
113
95
|
const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
|
|
114
96
|
debug("Opening workspace...");
|
|
115
97
|
await openAgentWorkspace({
|
|
@@ -126,7 +108,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
126
108
|
task,
|
|
127
109
|
repository,
|
|
128
110
|
agent,
|
|
129
|
-
worktreeDir
|
|
111
|
+
worktreeDir,
|
|
130
112
|
branchName,
|
|
131
113
|
workspaceName: task,
|
|
132
114
|
state: "running",
|
|
@@ -147,7 +129,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
147
129
|
task,
|
|
148
130
|
repository,
|
|
149
131
|
agent,
|
|
150
|
-
worktreeDir
|
|
132
|
+
worktreeDir,
|
|
151
133
|
branchName,
|
|
152
134
|
workspaceName: task,
|
|
153
135
|
state: "failed-to-launch",
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { orchestrate, type OrchestratorOptions } from "./commands/orchestrator.t
|
|
|
6
6
|
export { resumeWorkspace, type ResumeWorkspaceOptions } from "./commands/resumeWorkspace.ts";
|
|
7
7
|
export { setupWorkspace, type SetupWorkspaceOptions } from "./commands/setupWorkspace.ts";
|
|
8
8
|
export { status, type StatusOptions } from "./commands/status.ts";
|
|
9
|
-
export type { Config, HookCommands, AgentDefinition, ResolvedConfig, SourceConfig, } from "./lib/config.ts";
|
|
9
|
+
export type { Config, HookCommands, AgentDefinition, KnownRepository, ResolvedConfig, SourceConfig, } from "./lib/config.ts";
|
|
10
10
|
export { loadConfig } from "./lib/config.ts";
|
|
11
11
|
export { readRunState, recordRunState, removeRunState, runStateDirectory, runStatePath, updateRunState, type RunLifecycleState, type RunState, } from "./lib/runState.ts";
|
|
12
12
|
export { fetchBlockersForTask, fetchInProgressIssueCount, fetchRawLinearIssue, fetchResolvedIssue, isIssueInProgress, isIssueTodo, isTerminalStateType, isTerminalStatusForBlocker, isTerminalStatusForIssue, type RawLinearIssue, } from "./lib/adapters/linear/fetch.ts";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAClE,YAAY,EACV,MAAM,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,cAAc,GACpB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,oBAAoB,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,aAAa,EACb,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,KAAK,OAAO,IAAI,gBAAgB,EAChC,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,eAAe,EACpB,KAAK,eAAe,IAAI,wBAAwB,EAChD,KAAK,KAAK,IAAI,cAAc,EAC5B,iBAAiB,IAAI,0BAA0B,EAC/C,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,UAAU,GAChB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAClE,YAAY,EACV,MAAM,EACN,YAAY,EACZ,eAAe,EACf,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,cAAc,GACpB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,oBAAoB,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,aAAa,EACb,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,KAAK,OAAO,IAAI,gBAAgB,EAChC,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,eAAe,EACpB,KAAK,eAAe,IAAI,wBAAwB,EAChD,KAAK,KAAK,IAAI,cAAc,EAC5B,iBAAiB,IAAI,0BAA0B,EAC/C,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,UAAU,GAChB,MAAM,qBAAqB,CAAC"}
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { type LocalRunner, type AgentDefinition, type ResolvedConfig } from "./config.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Stage any srt settings and build the workspace launch command — the assembly
|
|
4
|
+
* shared verbatim by `setupWorkspace` (fresh runs) and `resumeWorkspace`
|
|
5
|
+
* (resumes). `worktreeDir` is the checkout root (srt grants + `{{worktree}}`);
|
|
6
|
+
* `workingDir` is the agent cwd (the worktree root, or its `workdir` subproject).
|
|
7
|
+
* Returns `srtSettingsDir` so callers can tear it down on a pre-launch failure.
|
|
8
|
+
*/
|
|
9
|
+
export declare function composeAgentLaunch(input: {
|
|
10
|
+
runner: LocalRunner;
|
|
11
|
+
task: string;
|
|
12
|
+
definition: AgentDefinition;
|
|
13
|
+
promptFile: string;
|
|
14
|
+
worktreeDir: string;
|
|
15
|
+
workingDir: string;
|
|
16
|
+
secretsFile?: string | undefined;
|
|
17
|
+
prepareWorktreeCommand?: string | undefined;
|
|
18
|
+
sandboxName?: string | undefined;
|
|
19
|
+
}): {
|
|
20
|
+
launchCommand: string;
|
|
21
|
+
srtSettingsDir: string | undefined;
|
|
22
|
+
};
|
|
2
23
|
interface PreparedAgentLaunch {
|
|
3
24
|
runner: LocalRunner;
|
|
4
25
|
sandboxName: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AASrB;;;;;;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;CAClC,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAwBhE;AAED,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,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,CA+C/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
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
import { ensureClearance } from "@clipboard-health/clearance";
|
|
2
|
+
import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
|
|
2
3
|
import { hasPreLaunchEnv, } from "./config.js";
|
|
3
4
|
import { detectHostCapabilities } from "./host.js";
|
|
5
|
+
import { buildLaunchCommand } from "./launchCommand.js";
|
|
4
6
|
import { assertLocalRunnerRequirements, resolveLocalRunner } from "./localRunner.js";
|
|
5
7
|
import { sandboxNameFor } from "./sandboxName.js";
|
|
8
|
+
import { buildAndStageSrtLaunch } from "./srtLaunch.js";
|
|
6
9
|
import { debug, sleep } from "./util.js";
|
|
7
10
|
import { workspaces } from "./workspaces.js";
|
|
11
|
+
/**
|
|
12
|
+
* Stage any srt settings and build the workspace launch command — the assembly
|
|
13
|
+
* shared verbatim by `setupWorkspace` (fresh runs) and `resumeWorkspace`
|
|
14
|
+
* (resumes). `worktreeDir` is the checkout root (srt grants + `{{worktree}}`);
|
|
15
|
+
* `workingDir` is the agent cwd (the worktree root, or its `workdir` subproject).
|
|
16
|
+
* Returns `srtSettingsDir` so callers can tear it down on a pre-launch failure.
|
|
17
|
+
*/
|
|
18
|
+
export function composeAgentLaunch(input) {
|
|
19
|
+
const staged = input.runner === "srt"
|
|
20
|
+
? buildAndStageSrtLaunch({
|
|
21
|
+
task: input.task,
|
|
22
|
+
worktreeDir: input.worktreeDir,
|
|
23
|
+
definition: input.definition,
|
|
24
|
+
})
|
|
25
|
+
: undefined;
|
|
26
|
+
const launchCommand = buildLaunchCommand({
|
|
27
|
+
definition: input.definition,
|
|
28
|
+
promptFile: input.promptFile,
|
|
29
|
+
worktreeDir: input.worktreeDir,
|
|
30
|
+
workingDir: input.workingDir,
|
|
31
|
+
secretsFile: input.secretsFile,
|
|
32
|
+
prepareWorktreeCommand: input.prepareWorktreeCommand,
|
|
33
|
+
runner: input.runner,
|
|
34
|
+
sandboxName: input.sandboxName,
|
|
35
|
+
srtPrepareSettingsFile: staged?.prepareFile,
|
|
36
|
+
srtAgentSettingsFile: staged?.agentFile,
|
|
37
|
+
srtSettingsDir: staged?.directory,
|
|
38
|
+
srtAgentConfigDirEnv: staged?.agentConfigDirEnv,
|
|
39
|
+
});
|
|
40
|
+
return { launchCommand, srtSettingsDir: staged?.directory };
|
|
41
|
+
}
|
|
8
42
|
export async function prepareAgentLaunch(input) {
|
|
9
43
|
const host = await detectHostCapabilities(input.signal);
|
|
10
44
|
const runner = resolveLocalRunner(input.config.local.runner, host);
|
|
@@ -44,6 +78,9 @@ async function alreadyReady() {
|
|
|
44
78
|
}
|
|
45
79
|
async function ensureSafehouseClearance(signal) {
|
|
46
80
|
await ensureClearance({
|
|
81
|
+
envOverrides: {
|
|
82
|
+
CLEARANCE_ALLOW_HOSTS_FILES: clearanceAllowHostsFilesFromEnvironment(),
|
|
83
|
+
},
|
|
47
84
|
logger: debug,
|
|
48
85
|
...(signal === undefined
|
|
49
86
|
? {}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface ClearanceAllowHostsFilesInput {
|
|
2
|
+
defaultFile?: string | undefined;
|
|
3
|
+
existingFiles?: string | undefined;
|
|
4
|
+
}
|
|
5
|
+
export declare function clearanceAllowHostsFilesValue(input?: ClearanceAllowHostsFilesInput): string;
|
|
6
|
+
export declare function clearanceAllowHostsFilesFromEnvironment(): string;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=clearanceAllowlist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clearanceAllowlist.d.ts","sourceRoot":"","sources":["../../src/lib/clearanceAllowlist.ts"],"names":[],"mappings":"AAOA,UAAU,6BAA6B;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAMD,wBAAgB,6BAA6B,CAAC,KAAK,GAAE,6BAAkC,GAAG,MAAM,CAa/F;AAED,wBAAgB,uCAAuC,IAAI,MAAM,CAIhE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { splitPathList } from "./pathList.js";
|
|
3
|
+
import { readEnvironmentVariable } from "./util.js";
|
|
4
|
+
const CLEARANCE_ALLOW_HOSTS_FILES = "CLEARANCE_ALLOW_HOSTS_FILES";
|
|
5
|
+
function groundcrewClearanceAllowHostsFile() {
|
|
6
|
+
return path.resolve(import.meta.dirname, "..", "..", "clearance-allow-hosts");
|
|
7
|
+
}
|
|
8
|
+
export function clearanceAllowHostsFilesValue(input = {}) {
|
|
9
|
+
const defaultFile = input.defaultFile ?? groundcrewClearanceAllowHostsFile();
|
|
10
|
+
const files = [defaultFile, ...splitPathList(input.existingFiles)];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const uniqueFiles = [];
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
if (seen.has(file)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
seen.add(file);
|
|
18
|
+
uniqueFiles.push(file);
|
|
19
|
+
}
|
|
20
|
+
return uniqueFiles.join(path.delimiter);
|
|
21
|
+
}
|
|
22
|
+
export function clearanceAllowHostsFilesFromEnvironment() {
|
|
23
|
+
return clearanceAllowHostsFilesValue({
|
|
24
|
+
existingFiles: readEnvironmentVariable(CLEARANCE_ALLOW_HOSTS_FILES),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* validates.
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
|
-
import
|
|
17
|
+
import { splitPathList } from "./pathList.js";
|
|
18
18
|
import { debug } from "./util.js";
|
|
19
19
|
/**
|
|
20
20
|
* Parse and validate clearance allow-host sources into a de-duplicated list of
|
|
@@ -51,15 +51,6 @@ export function collectAllowedDomains(input) {
|
|
|
51
51
|
}
|
|
52
52
|
return domains;
|
|
53
53
|
}
|
|
54
|
-
function splitPathList(value) {
|
|
55
|
-
if (value === undefined || value.length === 0) {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
return value
|
|
59
|
-
.split(path.delimiter)
|
|
60
|
-
.map((entry) => entry.trim())
|
|
61
|
-
.filter((entry) => entry.length > 0);
|
|
62
|
-
}
|
|
63
54
|
/**
|
|
64
55
|
* Split a host source into candidate tokens. Handles env-style
|
|
65
56
|
* comma/whitespace separators and file-style newline lists with `#` comments
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -129,14 +129,42 @@ type UserAgentDefinition = EnabledUserAgentDefinition;
|
|
|
129
129
|
* Linear's default "In Progress" / "In Review" status names disambiguate
|
|
130
130
|
* `started` workflow states; unmatched statuses fall back to `state.type`.
|
|
131
131
|
*/
|
|
132
|
+
/**
|
|
133
|
+
* Scripted provisioning templates for a repository. Both run via `sh -c` in
|
|
134
|
+
* place of the native git porcelain: `create` replaces `git worktree add`,
|
|
135
|
+
* `remove` replaces `git worktree remove`. Grouping them in one object makes the
|
|
136
|
+
* native/scripted split structural — an entry either has `provision` (scripted)
|
|
137
|
+
* or it doesn't (native) — and removes the need for a both-or-neither check.
|
|
138
|
+
*/
|
|
139
|
+
export interface ProvisionScripts {
|
|
140
|
+
/** Shell template run in place of `git worktree add`. */
|
|
141
|
+
create: string;
|
|
142
|
+
/** Shell template run in place of `git worktree remove`. */
|
|
143
|
+
remove: string;
|
|
144
|
+
}
|
|
132
145
|
/**
|
|
133
146
|
* A configured repository. The bare-string form keeps the repo under
|
|
134
147
|
* `workspace.projectDir`; the object form's optional `projectDirOverride`
|
|
135
|
-
* overrides that parent directory so repos can live in more than one place.
|
|
148
|
+
* overrides that parent directory so repos can live in more than one place. When
|
|
149
|
+
* a `provision` block is present (a *scripted* entry), groundcrew runs its
|
|
150
|
+
* templates via `sh -c` in place of `git worktree add`/`remove`; the `name` is
|
|
151
|
+
* then a logical handle and the physical clone is the template's concern (e.g.
|
|
152
|
+
* graft's own registry).
|
|
136
153
|
*/
|
|
137
154
|
export interface KnownRepository {
|
|
155
|
+
/** Logical repo name: the token tickets reference and the worktree dir basename. */
|
|
138
156
|
name: string;
|
|
157
|
+
/** Overrides the parent directory the source repo lives under (defaults to `projectDir`). */
|
|
139
158
|
projectDirOverride?: string;
|
|
159
|
+
/** Scripted provisioning templates; presence marks this a scripted entry. Mutually exclusive with `projectDirOverride`. */
|
|
160
|
+
provision?: ProvisionScripts;
|
|
161
|
+
/**
|
|
162
|
+
* Project subdirectory within the worktree. When set, the agent cwd, the
|
|
163
|
+
* `prepareWorktree` hook, and the `.groundcrew/config.json` lookup re-root to
|
|
164
|
+
* `<worktree>/<workdir>`. The worktree root itself (identity, sandbox access)
|
|
165
|
+
* is unchanged. Relative, no `..`.
|
|
166
|
+
*/
|
|
167
|
+
workdir?: string;
|
|
140
168
|
}
|
|
141
169
|
export interface Config {
|
|
142
170
|
/**
|
|
@@ -246,8 +274,10 @@ export interface ResolvedConfig {
|
|
|
246
274
|
projectDir: string;
|
|
247
275
|
/** Resolved worktree root; unset means "use projectDir". */
|
|
248
276
|
worktreeDir?: string;
|
|
249
|
-
/** Repository names only —
|
|
277
|
+
/** Repository names only — derived; what name-matching consumers read. */
|
|
250
278
|
knownRepositories: string[];
|
|
279
|
+
/** Normalized full entries carrying any `projectDirOverride`/`provision`. */
|
|
280
|
+
repositories: KnownRepository[];
|
|
251
281
|
/** name -> resolved parent dir, only for entries that override projectDir. */
|
|
252
282
|
repositoryDirs?: Record<string, string>;
|
|
253
283
|
};
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAO1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAE3F,MAAM,WAAW,YAAY;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,QAAQ,CAAC;AAE/B;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvE,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAKzD,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEtD,eAAO,MAAM,qBAAqB,EAAE,SAAS,kBAAkB,EAMrD,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,QAAQ,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACjD,CAAC;IACF;;;;OAIG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,KAAK,SAAS,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC;AAC/D,KAAK,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,GAAG;IAC1E,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB,CAAC;AACF,KAAK,mBAAmB,GAAG,0BAA0B,CAAC;AAEtD;;;;;;;;;GASG;AACH
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -466,46 +466,65 @@ function normalizeSources(raw) {
|
|
|
466
466
|
* per-repo options hang off — add new `KnownRepository` fields here.
|
|
467
467
|
*/
|
|
468
468
|
function normalizeKnownRepository(entry, index) {
|
|
469
|
+
const label = `workspace.knownRepositories[${index}]`;
|
|
469
470
|
if (typeof entry === "string") {
|
|
471
|
+
requireString(entry, label);
|
|
470
472
|
return { name: entry };
|
|
471
473
|
}
|
|
472
|
-
requireObject(entry,
|
|
473
|
-
requireString(entry.name,
|
|
474
|
-
|
|
475
|
-
|
|
474
|
+
requireObject(entry, label);
|
|
475
|
+
requireString(entry.name, `${label}.name`);
|
|
476
|
+
const recipe = { name: entry.name };
|
|
477
|
+
if (entry.projectDirOverride !== undefined) {
|
|
478
|
+
requireString(entry.projectDirOverride, `${label}.projectDirOverride`);
|
|
479
|
+
recipe.projectDirOverride = expandHome(entry.projectDirOverride);
|
|
480
|
+
}
|
|
481
|
+
if (entry.provision !== undefined) {
|
|
482
|
+
requireObject(entry.provision, `${label}.provision`);
|
|
483
|
+
const create = normalizeOptionalString(entry.provision.create, `${label}.provision.create`);
|
|
484
|
+
const remove = normalizeOptionalString(entry.provision.remove, `${label}.provision.remove`);
|
|
485
|
+
if (create === undefined || remove === undefined) {
|
|
486
|
+
fail(`${label}.provision must define both \`create\` and \`remove\` templates`);
|
|
487
|
+
}
|
|
488
|
+
recipe.provision = { create, remove };
|
|
489
|
+
}
|
|
490
|
+
const workdir = normalizeOptionalString(entry.workdir, `${label}.workdir`);
|
|
491
|
+
if (workdir !== undefined) {
|
|
492
|
+
recipe.workdir = workdir;
|
|
476
493
|
}
|
|
477
|
-
|
|
478
|
-
return { name: entry.name, projectDirOverride: expandHome(entry.projectDirOverride) };
|
|
494
|
+
return recipe;
|
|
479
495
|
}
|
|
480
496
|
/**
|
|
481
497
|
* Flatten the loose `(string | KnownRepository)[]` union into the strict
|
|
482
498
|
* resolved shape: a `string[]` of names every downstream consumer reads, plus
|
|
483
499
|
* a separate `repositoryDirs` map holding only the entries that override
|
|
484
|
-
* `projectDir`. Types are validated
|
|
485
|
-
* `expandHome` runs (
|
|
486
|
-
*
|
|
500
|
+
* `projectDir`. Types are validated at the resolution edge: every path is
|
|
501
|
+
* `requireString`-checked before its `expandHome` runs (here for `projectDir`/
|
|
502
|
+
* `worktreeDir`, and in `normalizeKnownRepository` for `projectDirOverride`),
|
|
503
|
+
* which would otherwise throw a raw TypeError on a non-string value.
|
|
487
504
|
*/
|
|
488
505
|
function normalizeWorkspace(workspace) {
|
|
489
506
|
requireObject(workspace, "workspace");
|
|
490
507
|
requireString(workspace.projectDir, "workspace.projectDir");
|
|
508
|
+
const entries = workspace.knownRepositories;
|
|
509
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
510
|
+
fail("workspace.knownRepositories must be a non-empty array");
|
|
511
|
+
}
|
|
512
|
+
const repositories = entries.map((entry, index) => normalizeKnownRepository(entry, index));
|
|
491
513
|
// Track the first index each name was seen at so a duplicate (which would
|
|
492
514
|
// silently overwrite its `projectDirOverride` in `repositoryDirs`) fails
|
|
493
515
|
// loudly instead of resolving order-dependently.
|
|
494
516
|
const seen = new Map();
|
|
495
517
|
const repositoryDirs = {};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const { name, projectDirOverride } = normalizeKnownRepository(entry, index);
|
|
499
|
-
const previous = seen.get(name);
|
|
518
|
+
repositories.forEach((recipe, index) => {
|
|
519
|
+
const previous = seen.get(recipe.name);
|
|
500
520
|
if (previous !== undefined) {
|
|
501
|
-
fail(`workspace.knownRepositories[${index}] duplicates ${JSON.stringify(name)} from workspace.knownRepositories[${previous}]. Configure distinct repository names.`);
|
|
521
|
+
fail(`workspace.knownRepositories[${index}] duplicates ${JSON.stringify(recipe.name)} from workspace.knownRepositories[${previous}]. Configure distinct repository names.`);
|
|
502
522
|
}
|
|
503
|
-
seen.set(name, index);
|
|
504
|
-
if (projectDirOverride !== undefined) {
|
|
505
|
-
repositoryDirs[name] = projectDirOverride;
|
|
523
|
+
seen.set(recipe.name, index);
|
|
524
|
+
if (recipe.projectDirOverride !== undefined) {
|
|
525
|
+
repositoryDirs[recipe.name] = recipe.projectDirOverride;
|
|
506
526
|
}
|
|
507
527
|
});
|
|
508
|
-
const names = [...seen.keys()];
|
|
509
528
|
let worktreeDir;
|
|
510
529
|
if (workspace.worktreeDir !== undefined) {
|
|
511
530
|
requireString(workspace.worktreeDir, "workspace.worktreeDir");
|
|
@@ -514,7 +533,8 @@ function normalizeWorkspace(workspace) {
|
|
|
514
533
|
return {
|
|
515
534
|
projectDir: expandHome(workspace.projectDir),
|
|
516
535
|
...(worktreeDir === undefined ? {} : { worktreeDir }),
|
|
517
|
-
knownRepositories:
|
|
536
|
+
knownRepositories: repositories.map((recipe) => recipe.name),
|
|
537
|
+
repositories,
|
|
518
538
|
...(Object.keys(repositoryDirs).length === 0 ? {} : { repositoryDirs }),
|
|
519
539
|
};
|
|
520
540
|
}
|
|
@@ -606,12 +626,19 @@ function validate(config) {
|
|
|
606
626
|
requireString(config.git.remote, "git.remote");
|
|
607
627
|
requireString(config.git.defaultBranch, "git.defaultBranch");
|
|
608
628
|
requireString(config.workspace.projectDir, "workspace.projectDir");
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
629
|
+
config.workspace.repositories.forEach((recipe, index) => {
|
|
630
|
+
const label = `workspace.knownRepositories[${index}]`;
|
|
631
|
+
if (recipe.projectDirOverride !== undefined && recipe.provision !== undefined) {
|
|
632
|
+
fail(`${label}.projectDirOverride cannot be combined with \`provision\`: a scripted entry has no groundcrew-managed clone, so \`projectDirOverride\` would be ignored`);
|
|
633
|
+
}
|
|
634
|
+
if (recipe.workdir !== undefined) {
|
|
635
|
+
if (path.isAbsolute(recipe.workdir)) {
|
|
636
|
+
fail(`${label}.workdir must be a relative path`);
|
|
637
|
+
}
|
|
638
|
+
if (recipe.workdir.split("/").includes("..")) {
|
|
639
|
+
fail(`${label}.workdir must not contain '..' segments`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
615
642
|
});
|
|
616
643
|
requirePositiveInt(config.orchestrator.maximumInProgress, "orchestrator.maximumInProgress");
|
|
617
644
|
requirePositiveInt(config.orchestrator.pollIntervalMilliseconds, "orchestrator.pollIntervalMilliseconds");
|
|
@@ -40,6 +40,13 @@ interface LaunchCommandArguments {
|
|
|
40
40
|
definition: AgentDefinition;
|
|
41
41
|
promptFile: string;
|
|
42
42
|
worktreeDir: string;
|
|
43
|
+
/**
|
|
44
|
+
* Directory the agent and prepareWorktree hook cwd into (the `cd`/`-w`
|
|
45
|
+
* target). Equals `worktreeDir` unless the repo recipe sets a `workdir`, in
|
|
46
|
+
* which case it is the subproject dir. The `{{worktree}}` template and the srt
|
|
47
|
+
* filesystem grants keep using `worktreeDir` (the whole checkout).
|
|
48
|
+
*/
|
|
49
|
+
workingDir: string;
|
|
43
50
|
/**
|
|
44
51
|
* Optional path to a `KEY='value'` env file containing build-time
|
|
45
52
|
* secrets (see `BUILD_SECRET_NAMES`). Sourced on the host shell before
|
|
@@ -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;
|
|
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;AA0MD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;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;CACpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA6B7E"}
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { BUILD_SECRET_NAMES, hasPreLaunchEnv, } from "./config.js";
|
|
5
|
+
import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
|
|
5
6
|
import { shellSingleQuote } from "./shell.js";
|
|
6
7
|
export { shellSingleQuote } from "./shell.js";
|
|
7
8
|
/**
|
|
@@ -100,6 +101,9 @@ function unsetEnvironmentLine(names) {
|
|
|
100
101
|
function unsetSecretsLine() {
|
|
101
102
|
return unsetEnvironmentLine(BUILD_SECRET_NAMES);
|
|
102
103
|
}
|
|
104
|
+
function safehouseClearanceWrapperCommand() {
|
|
105
|
+
return `CLEARANCE_ALLOW_HOSTS_FILES=${shellSingleQuote(clearanceAllowHostsFilesFromEnvironment())} ${shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH)}`;
|
|
106
|
+
}
|
|
103
107
|
function trapCleanupLine(promptDir) {
|
|
104
108
|
const cleanupCmd = `rm -rf ${shellSingleQuote(promptDir)}`;
|
|
105
109
|
return `trap ${shellSingleQuote(cleanupCmd)} EXIT`;
|
|
@@ -107,12 +111,13 @@ function trapCleanupLine(promptDir) {
|
|
|
107
111
|
/**
|
|
108
112
|
* Shared head of every host-shell `&&` chain: arm the `EXIT` trap that wipes
|
|
109
113
|
* `promptDir` (must come before any link that can fail, including the `cd`),
|
|
110
|
-
* then `cd` into the
|
|
111
|
-
*
|
|
112
|
-
*
|
|
114
|
+
* then `cd` into the working directory (the worktree root, or its `workdir`
|
|
115
|
+
* subproject). Kept separate from secret sourcing so the safehouse path can
|
|
116
|
+
* splice `preLaunch` between the `cd` and the secrets source — preLaunch must
|
|
117
|
+
* never see build-time secrets in env.
|
|
113
118
|
*/
|
|
114
119
|
function hostTrapAndCd(arguments_) {
|
|
115
|
-
return [trapCleanupLine(arguments_.promptDir), `cd ${shellSingleQuote(arguments_.
|
|
120
|
+
return [trapCleanupLine(arguments_.promptDir), `cd ${shellSingleQuote(arguments_.workingDir)}`];
|
|
116
121
|
}
|
|
117
122
|
/**
|
|
118
123
|
* Optional source-of-secrets line. Returns `[]` when no `secretsFile` is
|
|
@@ -301,7 +306,7 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
|
|
|
301
306
|
sandboxName: "",
|
|
302
307
|
});
|
|
303
308
|
const lines = [
|
|
304
|
-
...hostTrapAndCd({
|
|
309
|
+
...hostTrapAndCd({ workingDir: arguments_.workingDir, promptDir }),
|
|
305
310
|
...hostSourceSecrets(arguments_.secretsFile),
|
|
306
311
|
];
|
|
307
312
|
if (arguments_.prepareWorktreeCommand !== undefined) {
|
|
@@ -362,14 +367,14 @@ function buildSafehouseLaunchCommand(arguments_) {
|
|
|
362
367
|
const prepareWorktreeEnvPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
|
|
363
368
|
const preLaunchEnvNames = arguments_.definition.preLaunchEnv ?? [];
|
|
364
369
|
const agentEnvPassFlag = preLaunchEnvNames.length === 0 ? "" : `--env-pass=${preLaunchEnvNames.join(",")} `;
|
|
365
|
-
const safehouseWrapper =
|
|
370
|
+
const safehouseWrapper = safehouseClearanceWrapperCommand();
|
|
366
371
|
// Defensive shim+promptDir trap: by the time we arm it, `rm -rf <promptDir>`
|
|
367
372
|
// has already run (line below) so the promptDir wipe is a no-op on the happy
|
|
368
373
|
// path. Keeps the failure-window between shim creation and the explicit
|
|
369
374
|
// post-wrap cleanup covered for both targets without an unarmed window.
|
|
370
375
|
const shimAndPromptCleanup = `rm -rf "$_safehouse_shim_dir"; rm -rf ${shellSingleQuote(promptDir)}`;
|
|
371
376
|
const shimAndPromptTrap = `trap ${shellSingleQuote(shimAndPromptCleanup)} EXIT`;
|
|
372
|
-
const lines = hostTrapAndCd({
|
|
377
|
+
const lines = hostTrapAndCd({ workingDir: arguments_.workingDir, promptDir });
|
|
373
378
|
// Scrub inherited env before preLaunch (build secrets are copied out of
|
|
374
379
|
// `process.env`, which the launch shell inherits), then source secrets and
|
|
375
380
|
// read the staged prompt. See `hostPreLaunchSourceAndReadPrompt`.
|
|
@@ -486,7 +491,7 @@ function buildSrtLaunchCommand(arguments_) {
|
|
|
486
491
|
const cleanup = `rm -rf ${shellSingleQuote(arguments_.srtSettingsDir)}; rm -rf ${shellSingleQuote(promptDir)}`;
|
|
487
492
|
const lines = [
|
|
488
493
|
`trap ${shellSingleQuote(cleanup)} EXIT`,
|
|
489
|
-
`cd ${shellSingleQuote(arguments_.
|
|
494
|
+
`cd ${shellSingleQuote(arguments_.workingDir)}`,
|
|
490
495
|
...hostPreLaunchSourceAndReadPrompt({
|
|
491
496
|
definition: arguments_.definition,
|
|
492
497
|
worktreeDir: arguments_.worktreeDir,
|
|
@@ -528,6 +533,6 @@ function buildSdxLaunchCommand(arguments_) {
|
|
|
528
533
|
if (arguments_.secretsFile !== undefined) {
|
|
529
534
|
lines.push(sourceSecretsLine(arguments_.secretsFile));
|
|
530
535
|
}
|
|
531
|
-
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec sbx exec -it ${sbxEnvironmentFlags}-w ${shellSingleQuote(arguments_.
|
|
536
|
+
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec sbx exec -it ${sbxEnvironmentFlags}-w ${shellSingleQuote(arguments_.workingDir)} ${shellSingleQuote(arguments_.sandboxName)} sh -c ${shellSingleQuote(innerCommand)} sh "$_p"`);
|
|
532
537
|
return lines.join(" && ");
|
|
533
538
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pathList.d.ts","sourceRoot":"","sources":["../../src/lib/pathList.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CASjE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function splitPathList(value) {
|
|
3
|
+
const paths = [];
|
|
4
|
+
for (const entry of value?.split(path.delimiter) ?? []) {
|
|
5
|
+
const pathEntry = entry.trim();
|
|
6
|
+
if (pathEntry.length > 0) {
|
|
7
|
+
paths.push(pathEntry);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return paths;
|
|
11
|
+
}
|
package/dist/lib/srtLaunch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AgentDefinition } from "./config.ts";
|
|
2
2
|
export interface StagedSrtLaunch {
|
|
3
3
|
/** Dedicated temp dir holding the settings files (and any relocated config home). */
|
|
4
4
|
directory: string;
|
|
@@ -36,8 +36,6 @@ export interface StagedSrtLaunch {
|
|
|
36
36
|
* command tears the whole dir down after srt exits.
|
|
37
37
|
*/
|
|
38
38
|
export declare function buildAndStageSrtLaunch(input: {
|
|
39
|
-
config: ResolvedConfig;
|
|
40
|
-
repository: string;
|
|
41
39
|
task: string;
|
|
42
40
|
worktreeDir: string;
|
|
43
41
|
definition: AgentDefinition;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"
|
|
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;AAuBD;;;;;;;;;;;;;;;;;;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"}
|
package/dist/lib/srtLaunch.js
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
|
|
4
5
|
import { collectAllowedDomains } from "./clearanceHosts.js";
|
|
5
|
-
import {
|
|
6
|
+
import { runCommand } from "./commandRunner.js";
|
|
6
7
|
import { inferAgentCommandName } from "./launchCommand.js";
|
|
7
8
|
import { agentConfigRelocation, buildSrtSettings } from "./srtPolicy.js";
|
|
8
9
|
import { readEnvironmentVariable } from "./util.js";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the worktree's real git common dir — the shared `.git` the srt policy
|
|
12
|
+
* fences off (read-grant + narrow write-allow + write-denies). Derived from the
|
|
13
|
+
* worktree itself rather than assuming a `<projectDir>/<repo>/.git` clone, so
|
|
14
|
+
* scripted/sparse-checkout worktrees — whose checkout is owned by an external
|
|
15
|
+
* provisioner and whose `repo` is just an alias with no clone on disk — get the
|
|
16
|
+
* correct dir instead of a phantom path that breaks git access and leaves the
|
|
17
|
+
* real common dir unfenced. For native worktrees this returns the same
|
|
18
|
+
* `<projectDir>/<repo>/.git` as before. `--path-format=absolute` keeps the path
|
|
19
|
+
* absolute regardless of git version or cwd.
|
|
20
|
+
*/
|
|
21
|
+
function resolveGitCommonDir(worktreeDir) {
|
|
22
|
+
return runCommand("git", [
|
|
23
|
+
"-C",
|
|
24
|
+
worktreeDir,
|
|
25
|
+
"rev-parse",
|
|
26
|
+
"--path-format=absolute",
|
|
27
|
+
"--git-common-dir",
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
9
30
|
/**
|
|
10
31
|
* Generate the srt policies for a launch and stage them, plus — for agents that
|
|
11
32
|
* cannot run with a read-only config home (codex) — a relocated, writable
|
|
@@ -28,13 +49,12 @@ import { readEnvironmentVariable } from "./util.js";
|
|
|
28
49
|
export function buildAndStageSrtLaunch(input) {
|
|
29
50
|
const agent = inferAgentCommandName(input.definition.cmd);
|
|
30
51
|
const homeDir = input.homeDir ?? os.homedir();
|
|
31
|
-
const repoDir = path.resolve(repositoryBaseDir(input.config, input.repository), input.repository);
|
|
32
52
|
const base = {
|
|
33
53
|
worktreeDir: input.worktreeDir,
|
|
34
|
-
gitCommonDir:
|
|
54
|
+
gitCommonDir: resolveGitCommonDir(input.worktreeDir),
|
|
35
55
|
allowedDomains: collectAllowedDomains({
|
|
36
56
|
hosts: readEnvironmentVariable("CLEARANCE_ALLOW_HOSTS"),
|
|
37
|
-
files:
|
|
57
|
+
files: clearanceAllowHostsFilesFromEnvironment(),
|
|
38
58
|
}),
|
|
39
59
|
};
|
|
40
60
|
const directory = mkdtempSync(path.join(os.tmpdir(), `groundcrew-srt-${input.task}-`));
|
package/dist/lib/worktrees.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ export interface WorktreeSpec {
|
|
|
30
30
|
task: string;
|
|
31
31
|
}
|
|
32
32
|
declare function branchNameForTask(config: ResolvedConfig, task: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* The directory the agent and its setup hooks actually run in. Equals the
|
|
35
|
+
* worktree root unless the repo recipe sets a `workdir`, in which case it is
|
|
36
|
+
* `<worktreeDir>/<workdir>` — a monorepo subproject inside a sparse checkout.
|
|
37
|
+
* Pure path resolution; existence is enforced at create time by
|
|
38
|
+
* assertWorkdirPresent.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveLaunchDir(config: ResolvedConfig, repository: string, worktreeDir: string): string;
|
|
33
41
|
export type WorktreeDirtiness = {
|
|
34
42
|
kind: "dirty";
|
|
35
43
|
modified: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/worktrees.js
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* the module owns creation, listing, removal, and teardown (workspace-close +
|
|
9
9
|
* worktree-remove paired) so callers don't reach into git directly.
|
|
10
10
|
*/
|
|
11
|
-
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
11
|
+
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
12
12
|
import { userInfo } from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
|
+
import { applySubstitutions } from "./adapters/shell/invoke.js";
|
|
14
15
|
import { runCommandAsync } from "./commandRunner.js";
|
|
15
|
-
import { repositoryBaseDir, worktreeBaseDir } from "./config.js";
|
|
16
|
+
import { repositoryBaseDir, worktreeBaseDir, } from "./config.js";
|
|
16
17
|
import { resolveDefaultBranch } from "./defaultBranch.js";
|
|
17
18
|
import { assertPlainTaskId, isPlainTaskId } from "./taskId.js";
|
|
18
19
|
import { debug, errorMessage, isVerbose } from "./util.js";
|
|
@@ -43,22 +44,88 @@ function branchPrefix(config) {
|
|
|
43
44
|
function branchNameForTask(config, task) {
|
|
44
45
|
return `${branchPrefix(config)}-${task}`;
|
|
45
46
|
}
|
|
47
|
+
// Membership in knownRepositories is enforced by recipeFor (called first in
|
|
48
|
+
// basePaths), so this resolves the clone dir for a repo already known to exist
|
|
49
|
+
// in config and only guards against the clone being absent on disk.
|
|
46
50
|
function repoDirFor(config, repository) {
|
|
47
|
-
if (!config.workspace.knownRepositories.includes(repository)) {
|
|
48
|
-
throw new Error(`Repository "${repository}" is not in workspace.knownRepositories: ${config.workspace.knownRepositories.join(", ")}`);
|
|
49
|
-
}
|
|
50
51
|
const repoDir = path.resolve(repositoryBaseDir(config, repository), repository);
|
|
51
52
|
if (!existsSync(repoDir)) {
|
|
52
53
|
throw new Error(`Repository not found: ${repoDir}`);
|
|
53
54
|
}
|
|
54
55
|
return repoDir;
|
|
55
56
|
}
|
|
57
|
+
function recipeFor(config, repository) {
|
|
58
|
+
const recipe = config.workspace.repositories.find((entry) => entry.name === repository);
|
|
59
|
+
if (recipe === undefined) {
|
|
60
|
+
throw new Error(`Repository "${repository}" is not in workspace.knownRepositories: ${config.workspace.knownRepositories.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
return recipe;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The directory the agent and its setup hooks actually run in. Equals the
|
|
66
|
+
* worktree root unless the repo recipe sets a `workdir`, in which case it is
|
|
67
|
+
* `<worktreeDir>/<workdir>` — a monorepo subproject inside a sparse checkout.
|
|
68
|
+
* Pure path resolution; existence is enforced at create time by
|
|
69
|
+
* assertWorkdirPresent.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveLaunchDir(config, repository, worktreeDir) {
|
|
72
|
+
const recipe = recipeFor(config, repository);
|
|
73
|
+
return recipe.workdir === undefined ? worktreeDir : path.resolve(worktreeDir, recipe.workdir);
|
|
74
|
+
}
|
|
75
|
+
function assertWorkdirPresent(config, entry) {
|
|
76
|
+
const recipe = recipeFor(config, entry.repository);
|
|
77
|
+
if (recipe.workdir === undefined) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const launchDir = path.resolve(entry.dir, recipe.workdir);
|
|
81
|
+
if (!existsSync(launchDir) || !statSync(launchDir).isDirectory()) {
|
|
82
|
+
throw new Error(`Configured workdir "${recipe.workdir}" not found in worktree ${entry.dir}; the create template must produce it (looked for ${launchDir}).`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function provisionerSubstitutions(config, arguments_) {
|
|
86
|
+
return {
|
|
87
|
+
branch: arguments_.branchName,
|
|
88
|
+
dir: arguments_.dir,
|
|
89
|
+
baseRef: `${config.git.remote}/${config.git.defaultBranch}`,
|
|
90
|
+
repo: arguments_.repository,
|
|
91
|
+
task: arguments_.task,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Runs a provisioner template (`create`/`remove`) with no timeout. Mirrors
|
|
96
|
+
* runLongGitCommand: under --verbose the child streams live; otherwise it is
|
|
97
|
+
* captured and discarded on success (failures still carry stderr via the
|
|
98
|
+
* thrown error).
|
|
99
|
+
*/
|
|
100
|
+
async function runLongShellCommand(command, cwd, signal) {
|
|
101
|
+
const signalOption = signal === undefined ? {} : { signal };
|
|
102
|
+
if (isVerbose()) {
|
|
103
|
+
await runCommandAsync("sh", ["-c", command], {
|
|
104
|
+
cwd,
|
|
105
|
+
stdio: "inherit",
|
|
106
|
+
timeoutMs: 0,
|
|
107
|
+
...signalOption,
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await runCommandAsync("sh", ["-c", command], {
|
|
112
|
+
cwd,
|
|
113
|
+
stdio: "captured",
|
|
114
|
+
timeoutMs: 0,
|
|
115
|
+
...signalOption,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
56
118
|
function basePaths(config, repository, task) {
|
|
57
119
|
// Tasks must match the same shape the worktree discovery regexes use,
|
|
58
120
|
// so create()/list()/findByTask() agree on what's a valid worktree.
|
|
59
121
|
// This also rejects traversal tokens before they reach path.resolve().
|
|
60
122
|
assertPlainTaskId(task);
|
|
61
|
-
const
|
|
123
|
+
const recipe = recipeFor(config, repository);
|
|
124
|
+
// Scripted entries have no source clone — graft owns the checkout — so run
|
|
125
|
+
// templates with cwd = the worktree root and never resolve a clone dir.
|
|
126
|
+
const repoDir = recipe.provision === undefined
|
|
127
|
+
? repoDirFor(config, repository)
|
|
128
|
+
: path.resolve(worktreeBaseDir(config));
|
|
62
129
|
const hostWorktreeName = `${repository}-${task}`;
|
|
63
130
|
const hostWorktreeDir = path.resolve(worktreeBaseDir(config), hostWorktreeName);
|
|
64
131
|
return {
|
|
@@ -119,6 +186,24 @@ async function deleteBranchBestEffort(arguments_) {
|
|
|
119
186
|
}
|
|
120
187
|
async function createWorktree(config, spec, signal) {
|
|
121
188
|
const base = basePaths(config, spec.repository, spec.task);
|
|
189
|
+
const recipe = recipeFor(config, spec.repository);
|
|
190
|
+
if (recipe.provision !== undefined) {
|
|
191
|
+
const command = applySubstitutions(recipe.provision.create, provisionerSubstitutions(config, {
|
|
192
|
+
branchName: base.branchName,
|
|
193
|
+
dir: base.hostWorktreeDir,
|
|
194
|
+
task: spec.task,
|
|
195
|
+
repository: spec.repository,
|
|
196
|
+
}));
|
|
197
|
+
debug(`Provisioning worktree ${spec.repository}-${spec.task} via create template...`);
|
|
198
|
+
await runLongShellCommand(command, base.repoDir, signal);
|
|
199
|
+
return {
|
|
200
|
+
repository: spec.repository,
|
|
201
|
+
task: spec.task,
|
|
202
|
+
branchName: base.branchName,
|
|
203
|
+
dir: base.hostWorktreeDir,
|
|
204
|
+
kind: "host",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
122
207
|
const defaultBranch = await resolveDefaultBranch({
|
|
123
208
|
repoDir: base.repoDir,
|
|
124
209
|
remote: config.git.remote,
|
|
@@ -188,6 +273,11 @@ function listWorktrees(config) {
|
|
|
188
273
|
return entries;
|
|
189
274
|
}
|
|
190
275
|
async function removeWorktree(config, entry, options) {
|
|
276
|
+
const recipe = recipeFor(config, entry.repository);
|
|
277
|
+
if (recipe.provision !== undefined) {
|
|
278
|
+
await removeScriptedWorktree(config, entry, recipe.provision.remove, options);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
191
281
|
const repoDir = path.resolve(repositoryBaseDir(config, entry.repository), entry.repository);
|
|
192
282
|
if (existsSync(entry.dir)) {
|
|
193
283
|
debug(`Removing worktree ${entry.dir}${options.force ? " (--force)" : ""}...`);
|
|
@@ -223,15 +313,7 @@ async function removeWorktree(config, entry, options) {
|
|
|
223
313
|
removeOrphanWorktreeDirectory(config, entry);
|
|
224
314
|
}
|
|
225
315
|
else {
|
|
226
|
-
const dirtiness = await
|
|
227
|
-
if (dirtiness.kind === "dirty") {
|
|
228
|
-
throw new Error(describeDirtyWorktree({
|
|
229
|
-
task: entry.task,
|
|
230
|
-
dir: entry.dir,
|
|
231
|
-
modified: dirtiness.modified,
|
|
232
|
-
untracked: dirtiness.untracked,
|
|
233
|
-
}), { cause: error });
|
|
234
|
-
}
|
|
316
|
+
const dirtiness = await throwIfWorktreeDirty(entry, options.signal, error);
|
|
235
317
|
if (dirtiness.kind === "unknown") {
|
|
236
318
|
const registration = await probeWorktreeRegistration({
|
|
237
319
|
repoDir,
|
|
@@ -259,6 +341,31 @@ async function removeWorktree(config, entry, options) {
|
|
|
259
341
|
...signalProperty(options.signal),
|
|
260
342
|
});
|
|
261
343
|
}
|
|
344
|
+
async function removeScriptedWorktree(config, entry, removeTemplate, options) {
|
|
345
|
+
const worktreeRoot = path.resolve(worktreeBaseDir(config));
|
|
346
|
+
// A scripted worktree's teardown lives in the remove template (e.g. `graft rm`),
|
|
347
|
+
// which owns provisioner-side branch/metadata beyond the checkout dir. Run it
|
|
348
|
+
// even when the dir is already gone so that state is still cleaned up; only the
|
|
349
|
+
// dirtiness guard — which needs the dir to inspect — is skipped in that case.
|
|
350
|
+
const worktreeExists = existsSync(entry.dir);
|
|
351
|
+
if (worktreeExists && !options.force) {
|
|
352
|
+
// Keep the data-loss guard: a dirty worktree is not removed without --force.
|
|
353
|
+
// Fail closed when the dirtiness probe can't confirm the worktree is clean,
|
|
354
|
+
// so the remove template never runs over uncommitted work.
|
|
355
|
+
const dirtiness = await throwIfWorktreeDirty(entry, options.signal);
|
|
356
|
+
if (dirtiness.kind !== "clean") {
|
|
357
|
+
throw new Error(`Could not verify ${entry.dir} is clean; rerun with --force after manual inspection.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const command = applySubstitutions(removeTemplate, provisionerSubstitutions(config, {
|
|
361
|
+
branchName: entry.branchName,
|
|
362
|
+
dir: entry.dir,
|
|
363
|
+
task: entry.task,
|
|
364
|
+
repository: entry.repository,
|
|
365
|
+
}));
|
|
366
|
+
debug(`Removing worktree ${entry.dir} via remove template...`);
|
|
367
|
+
await runLongShellCommand(command, worktreeRoot, options.signal);
|
|
368
|
+
}
|
|
262
369
|
async function probeWorktreeDirtiness(worktreeDir, signal) {
|
|
263
370
|
let output;
|
|
264
371
|
try {
|
|
@@ -285,6 +392,24 @@ async function probeWorktreeDirtiness(worktreeDir, signal) {
|
|
|
285
392
|
}
|
|
286
393
|
return { kind: "dirty", modified, untracked };
|
|
287
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Probe a worktree and, when it has uncommitted work, throw the data-loss guard
|
|
397
|
+
* error. Returns the dirtiness so the git-native path can still branch on
|
|
398
|
+
* `unknown`. `cause` chains the underlying git failure when called from a catch.
|
|
399
|
+
*/
|
|
400
|
+
async function throwIfWorktreeDirty(entry, signal, cause) {
|
|
401
|
+
const dirtiness = await probeWorktreeDirtiness(entry.dir, signal);
|
|
402
|
+
if (dirtiness.kind === "dirty") {
|
|
403
|
+
const message = describeDirtyWorktree({
|
|
404
|
+
task: entry.task,
|
|
405
|
+
dir: entry.dir,
|
|
406
|
+
modified: dirtiness.modified,
|
|
407
|
+
untracked: dirtiness.untracked,
|
|
408
|
+
});
|
|
409
|
+
throw cause === undefined ? new Error(message) : new Error(message, { cause });
|
|
410
|
+
}
|
|
411
|
+
return dirtiness;
|
|
412
|
+
}
|
|
288
413
|
function describeDirtyWorktree(arguments_) {
|
|
289
414
|
const { task, dir, modified, untracked } = arguments_;
|
|
290
415
|
const parts = [];
|
|
@@ -351,7 +476,15 @@ async function create(config, spec, signal) {
|
|
|
351
476
|
if (existing !== undefined) {
|
|
352
477
|
throw new WorktreeAlreadyExistsError(existing.dir);
|
|
353
478
|
}
|
|
354
|
-
|
|
479
|
+
const entry = await createWorktree(config, spec, signal);
|
|
480
|
+
try {
|
|
481
|
+
assertWorkdirPresent(config, entry);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
await removeWorktree(config, entry, { force: true, ...signalProperty(signal) });
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
return entry;
|
|
355
488
|
}
|
|
356
489
|
async function remove(config, entry, options) {
|
|
357
490
|
await removeWorktree(config, entry, {
|
package/docs/configuration.md
CHANGED
|
@@ -37,6 +37,42 @@ Clean up existing worktrees before switching it, or temporarily unset
|
|
|
37
37
|
`worktreeDir` when you need `crew cleanup` to find worktrees created beside the
|
|
38
38
|
repos.
|
|
39
39
|
|
|
40
|
+
## Scripted Worktrees (Sparse Checkouts)
|
|
41
|
+
|
|
42
|
+
A `workspace.knownRepositories` entry can be an **object** instead of a string when you want groundcrew to provision the worktree with a custom command — for example a sparse checkout via `graft` — instead of `git worktree add`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
workspace: {
|
|
46
|
+
projectDir: "~/dev/groundcrew",
|
|
47
|
+
knownRepositories: [
|
|
48
|
+
"your-org/your-repo",
|
|
49
|
+
{
|
|
50
|
+
name: "billing",
|
|
51
|
+
provision: {
|
|
52
|
+
create: "graft new ${branch} billing --from ${baseRef} --dir ${dir}",
|
|
53
|
+
remove: "graft rm ${branch} -f",
|
|
54
|
+
},
|
|
55
|
+
workdir: "services/billing",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **`name`** is a logical name — it is the token matched in task descriptions and the basename of the per-task worktree directory. The physical clone is the command's concern, so several scripted entries can share one underlying clone.
|
|
62
|
+
- **`provision`** marks the entry as scripted. **`provision.create`** runs in place of `git worktree add`; **`provision.remove`** runs in place of `git worktree remove`. Both are required — a `provision` block missing either is rejected at config load. `projectDirOverride` (the per-repo source-directory override for native entries) cannot be combined with `provision`; a scripted entry has no groundcrew-managed clone, so it is rejected at config load.
|
|
63
|
+
- Both templates run on the host via `sh -c` with the working directory set to the worktree root (`worktreeDir`, defaulting to `projectDir`). They interpolate `${branch}`, `${dir}`, `${baseRef}` (`<remote>/<defaultBranch>`), `${repo}`, and `${task}`; each value is shell-quoted.
|
|
64
|
+
- **`workdir`** (optional) is a relative subdirectory of the worktree. When set, the agent's working directory, the `prepareWorktree` hook, and the `.groundcrew/config.json` lookup all re-root to `<worktree>/<workdir>` — use it when a sparse checkout materializes a monorepo whose project lives in a subdirectory (e.g. `uv sync` must run there). The worktree root is unchanged: it is still what create/remove/list operate on, and a sandboxed agent keeps full read/write access to the whole checkout. `workdir` must be relative with no `..` segments; if it is missing after checkout, `crew` fails fast.
|
|
65
|
+
- A dirty scripted worktree is still protected from data loss: `crew cleanup` refuses to run `remove` unless you pass `--force`. Orphan and branch cleanup are delegated to your `remove` template, since groundcrew does not track the scripted clone.
|
|
66
|
+
|
|
67
|
+
Set `graft` (or whatever tool your templates call) up once, outside groundcrew:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
graft repo add ~/dev/owner/monorepo
|
|
71
|
+
graft alias add billing services/billing libs/common
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`crew doctor` does not parse or validate provisioner templates; if your setup is more than a simple command, put it in a wrapper script and call that from `provision.create` / `provision.remove`.
|
|
75
|
+
|
|
40
76
|
## Config Discovery
|
|
41
77
|
|
|
42
78
|
Resolution order:
|
package/docs/runners.md
CHANGED
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
## Safehouse Clearance Allowlist
|
|
15
15
|
|
|
16
|
-
Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` on `http://127.0.0.1:19999` and runs the agent through the bundled `safehouse-clearance` wrapper.
|
|
16
|
+
Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` on `http://127.0.0.1:19999` and runs the agent through the bundled `safehouse-clearance` wrapper. Groundcrew automatically points clearance at its shipped starter allowlist, so a fresh install does not need a `CLEARANCE_ALLOW_HOSTS_FILES` export.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Groundcrew ships that starter file at `$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts`, covering model APIs, Linear, Notion, Slack, Datadog, GitHub, npm, and common dev tooling.
|
|
19
|
+
|
|
20
|
+
To add ad hoc hosts for one run, use `CLEARANCE_ALLOW_HOSTS`:
|
|
19
21
|
|
|
20
22
|
```bash
|
|
21
23
|
CLEARANCE_ALLOW_HOSTS="api.openai.com,auth.openai.com,api.anthropic.com,mcp.linear.app,api.linear.app" \
|
|
22
24
|
crew run --watch
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
To keep personal hosts in a file, set `CLEARANCE_ALLOW_HOSTS_FILES` to only the additional files. Groundcrew prepends its shipped file automatically:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
|
-
CLEARANCE_ALLOW_HOSTS_FILES="$
|
|
30
|
+
CLEARANCE_ALLOW_HOSTS_FILES="$HOME/.config/clearance/personal-allow-hosts" \
|
|
29
31
|
crew run --watch
|
|
30
32
|
```
|
|
31
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.31.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",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@anthropic-ai/sandbox-runtime": "0.0.52",
|
|
72
|
-
"@clipboard-health/clearance": "1.
|
|
72
|
+
"@clipboard-health/clearance": "1.2.0",
|
|
73
73
|
"@linear/sdk": "86.0.0",
|
|
74
74
|
"cosmiconfig": "9.0.1",
|
|
75
75
|
"tslib": "2.8.1",
|