@clipboard-health/groundcrew 4.3.1 → 4.3.3
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/writeback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,wBAAwB;IAChC,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,wBAAgB,8BAA8B,CAAC,UAAU,EAAE;IACzD,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,wBAAwB,
|
|
1
|
+
{"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/writeback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,wBAAwB;IAChC,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,wBAAgB,8BAA8B,CAAC,UAAU,EAAE;IACzD,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,wBAAwB,CAuD3B"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { log } from "../../util.js";
|
|
2
2
|
export function createLinearIssueStatusUpdater(arguments_) {
|
|
3
3
|
const { client } = arguments_;
|
|
4
|
-
// Positive cache only. Keyed by teamId because the
|
|
5
|
-
//
|
|
4
|
+
// Positive cache only. Keyed by teamId because the in-progress-state
|
|
5
|
+
// resolution yields a single stateId per team — independent of which
|
|
6
6
|
// project the ticket belongs to. State ids don't change for misconfig
|
|
7
7
|
// reasons, so caching successful resolutions is safe across the process.
|
|
8
8
|
//
|
|
@@ -23,10 +23,16 @@ export function createLinearIssueStatusUpdater(arguments_) {
|
|
|
23
23
|
}
|
|
24
24
|
const team = await client.team(teamId);
|
|
25
25
|
const states = await team.states();
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
26
|
+
// Linear's default workflow has MULTIPLE `started`-type states — both
|
|
27
|
+
// "In Progress" and "In Review" are `started`. `team.states()` orders by
|
|
28
|
+
// updatedAt (the connection has no position ordering), so array order
|
|
29
|
+
// can't disambiguate them. Prefer the state literally named "In Progress";
|
|
30
|
+
// otherwise fall back to the lowest-position (leftmost) `started` column,
|
|
31
|
+
// which by Linear convention is the in-progress one. This survives teams
|
|
32
|
+
// that rename the column ("Doing", "WIP", ...).
|
|
33
|
+
const startedStates = states.nodes.filter((state) => state.type === "started");
|
|
34
|
+
const inProgress = startedStates.find((state) => state.name.trim().toLowerCase() === "in progress") ??
|
|
35
|
+
startedStates.toSorted((a, b) => a.position - b.position).at(0);
|
|
30
36
|
if (inProgress?.id === undefined) {
|
|
31
37
|
return undefined;
|
|
32
38
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;GAGG;AACH,eAAO,MAAM,aAAa,mFACwD,CAAC;
|
|
1
|
+
{"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;GAGG;AACH,eAAO,MAAM,aAAa,mFACwD,CAAC;AAiHnF,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CAQ7E"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
3
|
import { BUILD_SECRET_NAMES } from "./config.js";
|
|
4
4
|
import { shellSingleQuote } from "./shell.js";
|
|
5
5
|
export { shellSingleQuote } from "./shell.js";
|
|
@@ -53,6 +53,77 @@ function sourceSecretsLine(secretsFile) {
|
|
|
53
53
|
function unsetSecretsLine() {
|
|
54
54
|
return `unset ${BUILD_SECRET_NAMES.join(" ")}`;
|
|
55
55
|
}
|
|
56
|
+
function tokenizeShellPrefix(command) {
|
|
57
|
+
const tokens = [];
|
|
58
|
+
let current = "";
|
|
59
|
+
let quote;
|
|
60
|
+
let isEscaped = false;
|
|
61
|
+
for (const character of command.trim()) {
|
|
62
|
+
if (isEscaped) {
|
|
63
|
+
current += character;
|
|
64
|
+
isEscaped = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (character === "\\" && quote !== "'") {
|
|
68
|
+
isEscaped = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (quote !== undefined) {
|
|
72
|
+
if (character === quote) {
|
|
73
|
+
quote = undefined;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
current += character;
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (character === "'" || character === '"') {
|
|
81
|
+
quote = character;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (/\s/.test(character)) {
|
|
85
|
+
if (current.length > 0) {
|
|
86
|
+
tokens.push(current);
|
|
87
|
+
current = "";
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
current += character;
|
|
92
|
+
}
|
|
93
|
+
if (current.length > 0) {
|
|
94
|
+
tokens.push(current);
|
|
95
|
+
}
|
|
96
|
+
return tokens;
|
|
97
|
+
}
|
|
98
|
+
function isEnvironmentAssignment(token) {
|
|
99
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
100
|
+
}
|
|
101
|
+
function safehouseProfileCommandName(agentCmd) {
|
|
102
|
+
const tokens = tokenizeShellPrefix(agentCmd);
|
|
103
|
+
let tokenIndex = tokens[0] === "env" ? 1 : 0;
|
|
104
|
+
if (tokens[0] === "env" && tokens[tokenIndex] === "--") {
|
|
105
|
+
tokenIndex += 1;
|
|
106
|
+
}
|
|
107
|
+
let commandToken;
|
|
108
|
+
for (const token of tokens.slice(tokenIndex)) {
|
|
109
|
+
if (isEnvironmentAssignment(token)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
commandToken = token;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (commandToken === undefined) {
|
|
116
|
+
throw new Error(`Cannot infer Safehouse agent profile command from model cmd ${JSON.stringify(agentCmd)}.`);
|
|
117
|
+
}
|
|
118
|
+
const commandName = basename(commandToken);
|
|
119
|
+
if (commandName === "." ||
|
|
120
|
+
commandName === ".." ||
|
|
121
|
+
commandName.startsWith("-") ||
|
|
122
|
+
!/^[A-Za-z0-9._-]+$/.test(commandName)) {
|
|
123
|
+
throw new Error(`Cannot use ${JSON.stringify(commandName)} as a Safehouse agent profile command name inferred from model cmd ${JSON.stringify(agentCmd)}.`);
|
|
124
|
+
}
|
|
125
|
+
return commandName;
|
|
126
|
+
}
|
|
56
127
|
/**
|
|
57
128
|
* Build the shell command that runs inside the workspace. The prompt is
|
|
58
129
|
* staged in a temp file (so backticks/quotes/$ in the description survive),
|
|
@@ -106,35 +177,46 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
|
|
|
106
177
|
return lines.join(" && ");
|
|
107
178
|
}
|
|
108
179
|
/**
|
|
109
|
-
* Safehouse launch. Setup runs *inside*
|
|
110
|
-
* the sdx runner) so the repo's `.groundcrew/setup.sh` and its
|
|
111
|
-
* filesystem-isolated and egress-restricted
|
|
180
|
+
* Safehouse launch. Setup runs *inside* a plain `safehouse-clearance` wrap
|
|
181
|
+
* (mirroring the sdx runner) so the repo's `.groundcrew/setup.sh` and its
|
|
182
|
+
* `npm install` are filesystem-isolated and egress-restricted without inheriting
|
|
183
|
+
* agent credentials/state grants. The agent then runs in a second Safehouse wrap
|
|
184
|
+
* through an agent-named shim so Safehouse can select only the agent profile.
|
|
112
185
|
*
|
|
113
186
|
* Build secrets are sourced into the host launch shell so Safehouse can forward
|
|
114
187
|
* them into the sandbox via `--env-pass` (Safehouse's `--env=FILE` mode otherwise
|
|
115
|
-
* strips them); they're `unset`
|
|
116
|
-
*
|
|
188
|
+
* strips them); they're `unset` on the host after setup and not passed to the
|
|
189
|
+
* agent wrap. The host keeps `cd`, the prompt read, and a temporary
|
|
190
|
+
* command-named shim so Safehouse can select the intended agent profile while
|
|
191
|
+
* the actual agent command remains `sh -lc`.
|
|
117
192
|
*/
|
|
118
193
|
function buildSafehouseLaunchCommand(arguments_) {
|
|
119
194
|
const promptDir = dirname(arguments_.promptFile);
|
|
195
|
+
const safehouseCommandName = safehouseProfileCommandName(arguments_.definition.cmd);
|
|
120
196
|
const agentCmd = renderAgentCommand({
|
|
121
197
|
agentCmd: arguments_.definition.cmd,
|
|
122
198
|
worktreeDir: arguments_.worktreeDir,
|
|
123
199
|
sandboxName: "",
|
|
124
200
|
});
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
innerParts.push(`exec ${agentCmd} "$@"`);
|
|
130
|
-
const innerCommand = innerParts.join("; ");
|
|
131
|
-
// Trailing space keeps the flag and `sh` separated; empty when no secrets.
|
|
201
|
+
const setupCommand = setupWithStatusReporting(SETUP_COMMAND);
|
|
202
|
+
const agentCommand = `exec ${agentCmd} "$@"`;
|
|
203
|
+
// Trailing space keeps the flag and setup command separated; empty when no secrets.
|
|
132
204
|
const envPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
|
|
205
|
+
const safehouseWrapper = shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH);
|
|
133
206
|
const lines = [`cd ${shellSingleQuote(arguments_.worktreeDir)}`];
|
|
134
207
|
if (arguments_.secretsFile !== undefined) {
|
|
135
208
|
lines.push(sourceSecretsLine(arguments_.secretsFile));
|
|
136
209
|
}
|
|
137
|
-
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`,
|
|
210
|
+
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `${safehouseWrapper} ${envPassFlag}sh -lc ${shellSingleQuote(setupCommand)}`);
|
|
211
|
+
if (arguments_.secretsFile !== undefined) {
|
|
212
|
+
lines.push(unsetSecretsLine());
|
|
213
|
+
}
|
|
214
|
+
lines.push(`_safehouse_shim_dir=$(mktemp -d "\${TMPDIR:-/tmp}/groundcrew-safehouse-XXXXXX")`, `trap 'rm -rf "$_safehouse_shim_dir"' EXIT`, `_safehouse_shim="$_safehouse_shim_dir/${safehouseCommandName}"`, `ln -s /bin/sh "$_safehouse_shim"`,
|
|
215
|
+
// Safehouse selects an agent profile from the wrapped command's basename.
|
|
216
|
+
// Running the real launch chain as `sh -lc` would make it see `sh`, so use
|
|
217
|
+
// an agent-named symlink to /bin/sh. This preserves per-agent profile
|
|
218
|
+
// selection without enabling every agent profile.
|
|
219
|
+
`${safehouseWrapper} "$_safehouse_shim" -lc ${shellSingleQuote(agentCommand)} sh "$_p"; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"`);
|
|
138
220
|
return lines.join(" && ");
|
|
139
221
|
}
|
|
140
222
|
function buildSdxLaunchCommand(arguments_) {
|
package/package.json
CHANGED