webmux 0.31.2 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/backend/dist/server.js +1180 -109
- package/bin/webmux.js +1777 -230
- package/frontend/dist/assets/{DiffDialog-DXkWdnXl.js → DiffDialog-CtwnOqjo.js} +1 -1
- package/frontend/dist/assets/index-CvURkZrd.css +1 -0
- package/frontend/dist/assets/index-EqF9CRFa.js +34 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-DLB0OmuO.js +0 -33
- package/frontend/dist/assets/index-HGkEqxw6.css +0 -1
package/backend/dist/server.js
CHANGED
|
@@ -6959,9 +6959,67 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6959
6959
|
|
|
6960
6960
|
// backend/src/server.ts
|
|
6961
6961
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6962
|
-
import { join as
|
|
6962
|
+
import { join as join7, resolve as resolve9 } from "path";
|
|
6963
6963
|
import { mkdirSync } from "fs";
|
|
6964
6964
|
import { networkInterfaces } from "os";
|
|
6965
|
+
// package.json
|
|
6966
|
+
var package_default = {
|
|
6967
|
+
name: "webmux",
|
|
6968
|
+
version: "0.32.0",
|
|
6969
|
+
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
6970
|
+
type: "module",
|
|
6971
|
+
repository: {
|
|
6972
|
+
type: "git",
|
|
6973
|
+
url: "git+https://github.com/windmill-labs/workmux-web.git"
|
|
6974
|
+
},
|
|
6975
|
+
homepage: "https://github.com/windmill-labs/workmux-web",
|
|
6976
|
+
keywords: [
|
|
6977
|
+
"workmux",
|
|
6978
|
+
"git-worktree",
|
|
6979
|
+
"tmux",
|
|
6980
|
+
"dashboard",
|
|
6981
|
+
"terminal",
|
|
6982
|
+
"ai-agent"
|
|
6983
|
+
],
|
|
6984
|
+
bin: {
|
|
6985
|
+
webmux: "bin/webmux.js"
|
|
6986
|
+
},
|
|
6987
|
+
workspaces: [
|
|
6988
|
+
"backend",
|
|
6989
|
+
"frontend",
|
|
6990
|
+
"packages/*"
|
|
6991
|
+
],
|
|
6992
|
+
dependencies: {},
|
|
6993
|
+
scripts: {
|
|
6994
|
+
dev: "bash dev.sh",
|
|
6995
|
+
start: "bun bin/webmux.js",
|
|
6996
|
+
build: "cd frontend && bun run build && cd .. && bun build backend/src/server.ts --target=bun --outfile=backend/dist/server.js && bun build bin/src/webmux.ts --target=bun --outfile=bin/webmux.js",
|
|
6997
|
+
prepublishOnly: "bun run build",
|
|
6998
|
+
test: "bun run --cwd backend test && bun test packages/api-contract/src && bun test bin/src && bun run --cwd frontend test",
|
|
6999
|
+
"test:coverage": "bun run --cwd backend test --coverage && bun test --coverage packages/api-contract/src && bun test --coverage bin/src && bun run --cwd frontend test:coverage"
|
|
7000
|
+
},
|
|
7001
|
+
files: [
|
|
7002
|
+
"bin/webmux.js",
|
|
7003
|
+
"backend/dist/",
|
|
7004
|
+
"frontend/dist/"
|
|
7005
|
+
],
|
|
7006
|
+
devDependencies: {
|
|
7007
|
+
"@webmux/api-contract": "workspace:*",
|
|
7008
|
+
"@clack/prompts": "^1.1.0",
|
|
7009
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
7010
|
+
"@tailwindcss/vite": "^4.2.0",
|
|
7011
|
+
"@types/bun": "latest",
|
|
7012
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
7013
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
7014
|
+
"@xterm/xterm": "^5.5.0",
|
|
7015
|
+
svelte: "^5.0.0",
|
|
7016
|
+
"svelte-check": "^4.0.0",
|
|
7017
|
+
tailwindcss: "^4.2.0",
|
|
7018
|
+
typescript: "^5.0.0",
|
|
7019
|
+
vite: "^6.0.0"
|
|
7020
|
+
},
|
|
7021
|
+
license: "MIT"
|
|
7022
|
+
};
|
|
6965
7023
|
|
|
6966
7024
|
// node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
|
|
6967
7025
|
var exports_external = {};
|
|
@@ -10936,7 +10994,7 @@ var coerce = {
|
|
|
10936
10994
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
10937
10995
|
};
|
|
10938
10996
|
var NEVER = INVALID;
|
|
10939
|
-
// node_modules/.bun/@ts-rest+core@3.52.1+
|
|
10997
|
+
// node_modules/.bun/@ts-rest+core@3.52.1+596964f7fee2c930/node_modules/@ts-rest/core/index.esm.mjs
|
|
10940
10998
|
var isZodObjectStrict = (obj) => {
|
|
10941
10999
|
return typeof (obj === null || obj === undefined ? undefined : obj.passthrough) === "function";
|
|
10942
11000
|
};
|
|
@@ -11028,6 +11086,30 @@ var EnabledResponseSchema = exports_external.object({
|
|
|
11028
11086
|
var BuiltInAgentIdSchema = exports_external.enum(["claude", "codex"]);
|
|
11029
11087
|
var AgentIdSchema = exports_external.string().trim().min(1);
|
|
11030
11088
|
var WorktreeCreateModeSchema = exports_external.enum(["new", "existing"]);
|
|
11089
|
+
var LinearIssueIdSchema = exports_external.string().regex(/^[A-Z]+-\d+$/, "Expected Linear issue id (e.g. ENG-123)");
|
|
11090
|
+
var LinearTeamKeySchema = exports_external.string().regex(/^[A-Z]+$/, "Expected Linear team key (e.g. ENG)");
|
|
11091
|
+
var PostWorktreeToLinearTargetSchema = exports_external.discriminatedUnion("kind", [
|
|
11092
|
+
exports_external.object({ kind: exports_external.literal("issue"), issueId: LinearIssueIdSchema }),
|
|
11093
|
+
exports_external.object({ kind: exports_external.literal("team"), teamKey: LinearTeamKeySchema, title: exports_external.string().trim().min(1).optional() })
|
|
11094
|
+
]);
|
|
11095
|
+
var PostWorktreeToLinearRequestSchema = exports_external.object({
|
|
11096
|
+
target: PostWorktreeToLinearTargetSchema
|
|
11097
|
+
});
|
|
11098
|
+
var PostWorktreeToLinearResponseSchema = exports_external.object({
|
|
11099
|
+
ok: exports_external.literal(true),
|
|
11100
|
+
issueId: exports_external.string(),
|
|
11101
|
+
issueUrl: exports_external.string(),
|
|
11102
|
+
commentUrl: exports_external.string().nullable(),
|
|
11103
|
+
attachmentUrl: exports_external.string()
|
|
11104
|
+
});
|
|
11105
|
+
var FromLinearInputSchema = exports_external.object({
|
|
11106
|
+
issueId: LinearIssueIdSchema,
|
|
11107
|
+
conversationContext: exports_external.string().optional()
|
|
11108
|
+
});
|
|
11109
|
+
var OneshotConfigSchema = exports_external.object({
|
|
11110
|
+
autoCloseOnDone: exports_external.boolean().optional(),
|
|
11111
|
+
postToLinearOnDone: PostWorktreeToLinearTargetSchema.optional()
|
|
11112
|
+
});
|
|
11031
11113
|
var AgentCapabilitiesSchema = exports_external.object({
|
|
11032
11114
|
terminal: exports_external.literal(true),
|
|
11033
11115
|
inAppChat: exports_external.boolean(),
|
|
@@ -11084,6 +11166,7 @@ var NumberLikePathParamSchema = exports_external.union([
|
|
|
11084
11166
|
var BranchListResponseSchema = exports_external.object({
|
|
11085
11167
|
branches: exports_external.array(AvailableBranchSchema)
|
|
11086
11168
|
});
|
|
11169
|
+
var WorktreeSourceSchema = exports_external.enum(["ui", "oneshot"]);
|
|
11087
11170
|
var CreateWorktreeRequestSchema = exports_external.object({
|
|
11088
11171
|
mode: WorktreeCreateModeSchema.optional(),
|
|
11089
11172
|
branch: exports_external.string().optional(),
|
|
@@ -11094,7 +11177,14 @@ var CreateWorktreeRequestSchema = exports_external.object({
|
|
|
11094
11177
|
prompt: exports_external.string().optional(),
|
|
11095
11178
|
envOverrides: exports_external.record(exports_external.string()).optional(),
|
|
11096
11179
|
createLinearTicket: exports_external.literal(true).optional(),
|
|
11097
|
-
linearTitle: exports_external.string().optional()
|
|
11180
|
+
linearTitle: exports_external.string().optional(),
|
|
11181
|
+
fromLinear: FromLinearInputSchema.optional(),
|
|
11182
|
+
source: WorktreeSourceSchema.optional(),
|
|
11183
|
+
oneshot: OneshotConfigSchema.optional()
|
|
11184
|
+
});
|
|
11185
|
+
var OpenWorktreeRequestSchema = exports_external.object({
|
|
11186
|
+
prompt: exports_external.string().optional(),
|
|
11187
|
+
oneshot: OneshotConfigSchema.optional()
|
|
11098
11188
|
});
|
|
11099
11189
|
var CreateWorktreeResponseSchema = exports_external.object({
|
|
11100
11190
|
primaryBranch: exports_external.string(),
|
|
@@ -11240,7 +11330,9 @@ var ProjectWorktreeSnapshotSchema = exports_external.object({
|
|
|
11240
11330
|
services: exports_external.array(ServiceStatusSchema),
|
|
11241
11331
|
prs: exports_external.array(PrEntrySchema),
|
|
11242
11332
|
linearIssue: LinkedLinearIssueSchema.nullable(),
|
|
11243
|
-
creation: WorktreeCreationStateSchema.nullable()
|
|
11333
|
+
creation: WorktreeCreationStateSchema.nullable(),
|
|
11334
|
+
source: WorktreeSourceSchema,
|
|
11335
|
+
oneshot: OneshotConfigSchema.nullable()
|
|
11244
11336
|
});
|
|
11245
11337
|
var ProjectSnapshotSchema = exports_external.object({
|
|
11246
11338
|
project: exports_external.object({
|
|
@@ -11289,13 +11381,16 @@ var AgentsUiWorktreeSummarySchema = exports_external.object({
|
|
|
11289
11381
|
});
|
|
11290
11382
|
var AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
|
|
11291
11383
|
var AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
|
|
11384
|
+
var AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
|
|
11292
11385
|
var AgentsUiConversationMessageSchema = exports_external.object({
|
|
11293
11386
|
id: exports_external.string(),
|
|
11294
11387
|
turnId: exports_external.string(),
|
|
11295
11388
|
role: AgentsUiConversationMessageRoleSchema,
|
|
11296
11389
|
text: exports_external.string(),
|
|
11297
11390
|
status: AgentsUiConversationMessageStatusSchema,
|
|
11298
|
-
createdAt: exports_external.string().nullable()
|
|
11391
|
+
createdAt: exports_external.string().nullable(),
|
|
11392
|
+
kind: AgentsUiConversationMessageKindSchema.optional(),
|
|
11393
|
+
toolName: exports_external.string().optional()
|
|
11299
11394
|
});
|
|
11300
11395
|
var AgentsUiConversationStateSchema = exports_external.object({
|
|
11301
11396
|
provider: WorktreeConversationProviderSchema,
|
|
@@ -11419,6 +11514,8 @@ var apiPaths = {
|
|
|
11419
11514
|
openWorktree: "/api/worktrees/:name/open",
|
|
11420
11515
|
closeWorktree: "/api/worktrees/:name/close",
|
|
11421
11516
|
setWorktreeArchived: "/api/worktrees/:name/archive",
|
|
11517
|
+
syncWorktreePrs: "/api/worktrees/:name/sync-prs",
|
|
11518
|
+
postWorktreeToLinear: "/api/worktrees/:name/linear/post",
|
|
11422
11519
|
setWorktreeLabel: "/api/worktrees/:name/label",
|
|
11423
11520
|
sendWorktreePrompt: "/api/worktrees/:name/send",
|
|
11424
11521
|
mergeWorktree: "/api/worktrees/:name/merge",
|
|
@@ -11597,7 +11694,7 @@ var apiContract = c.router({
|
|
|
11597
11694
|
method: "POST",
|
|
11598
11695
|
path: apiPaths.openWorktree,
|
|
11599
11696
|
pathParams: WorktreeNameParamsSchema,
|
|
11600
|
-
body:
|
|
11697
|
+
body: OpenWorktreeRequestSchema,
|
|
11601
11698
|
responses: {
|
|
11602
11699
|
200: OkResponseSchema,
|
|
11603
11700
|
...commonErrorResponses
|
|
@@ -11623,6 +11720,26 @@ var apiContract = c.router({
|
|
|
11623
11720
|
...commonErrorResponses
|
|
11624
11721
|
}
|
|
11625
11722
|
},
|
|
11723
|
+
postWorktreeToLinear: {
|
|
11724
|
+
method: "POST",
|
|
11725
|
+
path: apiPaths.postWorktreeToLinear,
|
|
11726
|
+
pathParams: WorktreeNameParamsSchema,
|
|
11727
|
+
body: PostWorktreeToLinearRequestSchema,
|
|
11728
|
+
responses: {
|
|
11729
|
+
200: PostWorktreeToLinearResponseSchema,
|
|
11730
|
+
...commonErrorResponses
|
|
11731
|
+
}
|
|
11732
|
+
},
|
|
11733
|
+
syncWorktreePrs: {
|
|
11734
|
+
method: "POST",
|
|
11735
|
+
path: apiPaths.syncWorktreePrs,
|
|
11736
|
+
pathParams: WorktreeNameParamsSchema,
|
|
11737
|
+
body: c.noBody(),
|
|
11738
|
+
responses: {
|
|
11739
|
+
200: ProjectWorktreeSnapshotSchema,
|
|
11740
|
+
...commonErrorResponses
|
|
11741
|
+
}
|
|
11742
|
+
},
|
|
11626
11743
|
setWorktreeLabel: {
|
|
11627
11744
|
method: "PUT",
|
|
11628
11745
|
path: apiPaths.setWorktreeLabel,
|
|
@@ -12066,26 +12183,43 @@ function isRecord(raw) {
|
|
|
12066
12183
|
function readString(raw) {
|
|
12067
12184
|
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
12068
12185
|
}
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
return
|
|
12186
|
+
var TOOL_PAYLOAD_TRUNCATE_LIMIT = 2000;
|
|
12187
|
+
function compactJson(value) {
|
|
12188
|
+
try {
|
|
12189
|
+
return JSON.stringify(value);
|
|
12190
|
+
} catch {
|
|
12191
|
+
return String(value);
|
|
12075
12192
|
}
|
|
12076
|
-
|
|
12193
|
+
}
|
|
12194
|
+
function truncate(text, limit = TOOL_PAYLOAD_TRUNCATE_LIMIT) {
|
|
12195
|
+
if (text.length <= limit)
|
|
12196
|
+
return text;
|
|
12197
|
+
return `${text.slice(0, limit)}\u2026 (truncated, ${text.length - limit} more chars)`;
|
|
12198
|
+
}
|
|
12199
|
+
function extractToolResultText(content) {
|
|
12200
|
+
if (typeof content === "string")
|
|
12201
|
+
return truncate(content.trim());
|
|
12202
|
+
if (!Array.isArray(content))
|
|
12203
|
+
return truncate(compactJson(content));
|
|
12204
|
+
const text = content.map((entry) => {
|
|
12077
12205
|
if (!isRecord(entry))
|
|
12078
12206
|
return "";
|
|
12079
|
-
if (entry.type
|
|
12080
|
-
return
|
|
12081
|
-
return
|
|
12207
|
+
if (entry.type === "text" && typeof entry.text === "string")
|
|
12208
|
+
return entry.text;
|
|
12209
|
+
return compactJson(entry);
|
|
12082
12210
|
}).join("").trim();
|
|
12211
|
+
return truncate(text);
|
|
12083
12212
|
}
|
|
12084
12213
|
function isTopLevelClaudeUserPrompt(raw) {
|
|
12085
12214
|
if (raw.type !== "user" || !isRecord(raw.message))
|
|
12086
12215
|
return false;
|
|
12087
12216
|
return raw.message.role === "user" && typeof raw.message.content === "string" && typeof raw.uuid === "string" && raw.message.content.trim().length > 0;
|
|
12088
12217
|
}
|
|
12218
|
+
function isClaudeUserToolResultRecord(raw) {
|
|
12219
|
+
if (raw.type !== "user" || !isRecord(raw.message))
|
|
12220
|
+
return false;
|
|
12221
|
+
return raw.message.role === "user" && Array.isArray(raw.message.content) && typeof raw.uuid === "string";
|
|
12222
|
+
}
|
|
12089
12223
|
function isClaudeAssistantRecord(raw) {
|
|
12090
12224
|
if (raw.type !== "assistant" || !isRecord(raw.message))
|
|
12091
12225
|
return false;
|
|
@@ -12132,11 +12266,10 @@ async function findClaudeSessionPath(sessionId, cwd) {
|
|
|
12132
12266
|
}
|
|
12133
12267
|
function parseClaudeSessionRecords(text) {
|
|
12134
12268
|
return text.split(`
|
|
12135
|
-
`).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
|
|
12269
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0 && line.startsWith("{")).flatMap((line) => {
|
|
12136
12270
|
try {
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
} catch (error) {
|
|
12271
|
+
return [JSON.parse(line)];
|
|
12272
|
+
} catch {
|
|
12140
12273
|
log.warn(`[agents] failed to parse Claude session line: ${line.slice(0, 120)}`);
|
|
12141
12274
|
return [];
|
|
12142
12275
|
}
|
|
@@ -12144,12 +12277,17 @@ function parseClaudeSessionRecords(text) {
|
|
|
12144
12277
|
}
|
|
12145
12278
|
function buildClaudeSessionFromText(input) {
|
|
12146
12279
|
const records = parseClaudeSessionRecords(input.text);
|
|
12147
|
-
const
|
|
12280
|
+
const messages = [];
|
|
12148
12281
|
let cwd = null;
|
|
12149
12282
|
let gitBranch = null;
|
|
12150
12283
|
let createdAt = null;
|
|
12151
12284
|
let lastSeenAt = null;
|
|
12152
|
-
let
|
|
12285
|
+
let currentTurnId = null;
|
|
12286
|
+
let blockIndex = 0;
|
|
12287
|
+
const pushMessage = (message) => {
|
|
12288
|
+
messages.push(message);
|
|
12289
|
+
blockIndex += 1;
|
|
12290
|
+
};
|
|
12153
12291
|
for (const record of records) {
|
|
12154
12292
|
cwd ??= readString(record.cwd);
|
|
12155
12293
|
gitBranch ??= readString(record.gitBranch);
|
|
@@ -12158,38 +12296,74 @@ function buildClaudeSessionFromText(input) {
|
|
|
12158
12296
|
}
|
|
12159
12297
|
lastSeenAt = readString(record.timestamp) ?? lastSeenAt;
|
|
12160
12298
|
if (isTopLevelClaudeUserPrompt(record)) {
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12299
|
+
currentTurnId = record.uuid;
|
|
12300
|
+
blockIndex = 0;
|
|
12301
|
+
pushMessage({
|
|
12302
|
+
id: record.uuid,
|
|
12303
|
+
turnId: record.uuid,
|
|
12304
|
+
role: "user",
|
|
12305
|
+
kind: "text",
|
|
12306
|
+
text: record.message.content.trim(),
|
|
12307
|
+
createdAt: readString(record.timestamp)
|
|
12308
|
+
});
|
|
12309
|
+
continue;
|
|
12310
|
+
}
|
|
12311
|
+
if (!currentTurnId)
|
|
12312
|
+
continue;
|
|
12313
|
+
if (isClaudeUserToolResultRecord(record)) {
|
|
12314
|
+
for (const entry of record.message.content) {
|
|
12315
|
+
if (!isRecord(entry) || entry.type !== "tool_result")
|
|
12316
|
+
continue;
|
|
12317
|
+
const text = extractToolResultText(entry.content);
|
|
12318
|
+
if (text.length === 0)
|
|
12319
|
+
continue;
|
|
12320
|
+
pushMessage({
|
|
12321
|
+
id: `${record.uuid}:${blockIndex}`,
|
|
12322
|
+
turnId: currentTurnId,
|
|
12168
12323
|
role: "user",
|
|
12169
|
-
|
|
12324
|
+
kind: "toolResult",
|
|
12325
|
+
text,
|
|
12170
12326
|
createdAt: readString(record.timestamp)
|
|
12171
|
-
}
|
|
12172
|
-
|
|
12173
|
-
};
|
|
12327
|
+
});
|
|
12328
|
+
}
|
|
12174
12329
|
continue;
|
|
12175
12330
|
}
|
|
12176
|
-
if (!
|
|
12331
|
+
if (!isClaudeAssistantRecord(record))
|
|
12177
12332
|
continue;
|
|
12178
|
-
|
|
12179
|
-
const text = extractClaudeMessageText(record.message.content);
|
|
12180
|
-
if (text.length === 0 || record.message.stop_reason !== "end_turn") {
|
|
12333
|
+
if (!Array.isArray(record.message.content))
|
|
12181
12334
|
continue;
|
|
12335
|
+
for (const block of record.message.content) {
|
|
12336
|
+
if (!isRecord(block))
|
|
12337
|
+
continue;
|
|
12338
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
12339
|
+
const text = block.text.trim();
|
|
12340
|
+
if (text.length === 0)
|
|
12341
|
+
continue;
|
|
12342
|
+
pushMessage({
|
|
12343
|
+
id: `${record.uuid}:${blockIndex}`,
|
|
12344
|
+
turnId: currentTurnId,
|
|
12345
|
+
role: "assistant",
|
|
12346
|
+
kind: "text",
|
|
12347
|
+
text,
|
|
12348
|
+
createdAt: readString(record.timestamp)
|
|
12349
|
+
});
|
|
12350
|
+
continue;
|
|
12351
|
+
}
|
|
12352
|
+
if (block.type === "tool_use") {
|
|
12353
|
+
const toolName = typeof block.name === "string" ? block.name : "tool";
|
|
12354
|
+
const text = truncate(compactJson(block.input ?? {}));
|
|
12355
|
+
pushMessage({
|
|
12356
|
+
id: `${record.uuid}:${blockIndex}`,
|
|
12357
|
+
turnId: currentTurnId,
|
|
12358
|
+
role: "assistant",
|
|
12359
|
+
kind: "toolUse",
|
|
12360
|
+
toolName,
|
|
12361
|
+
text,
|
|
12362
|
+
createdAt: readString(record.timestamp)
|
|
12363
|
+
});
|
|
12364
|
+
continue;
|
|
12365
|
+
}
|
|
12182
12366
|
}
|
|
12183
|
-
currentTurn.assistant = {
|
|
12184
|
-
id: record.uuid,
|
|
12185
|
-
turnId: currentTurn.user.turnId,
|
|
12186
|
-
role: "assistant",
|
|
12187
|
-
text,
|
|
12188
|
-
createdAt: readString(record.timestamp)
|
|
12189
|
-
};
|
|
12190
|
-
}
|
|
12191
|
-
if (currentTurn) {
|
|
12192
|
-
turns.push(currentTurn);
|
|
12193
12367
|
}
|
|
12194
12368
|
return {
|
|
12195
12369
|
sessionId: input.sessionId,
|
|
@@ -12198,7 +12372,7 @@ function buildClaudeSessionFromText(input) {
|
|
|
12198
12372
|
gitBranch,
|
|
12199
12373
|
createdAt,
|
|
12200
12374
|
lastSeenAt,
|
|
12201
|
-
messages
|
|
12375
|
+
messages
|
|
12202
12376
|
};
|
|
12203
12377
|
}
|
|
12204
12378
|
|
|
@@ -12824,6 +12998,15 @@ var DEFAULT_PANES = [
|
|
|
12824
12998
|
{ id: "agent", kind: "agent", focus: true },
|
|
12825
12999
|
{ id: "shell", kind: "shell", split: "right", sizePct: 25 }
|
|
12826
13000
|
];
|
|
13001
|
+
function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
|
|
13002
|
+
return [
|
|
13003
|
+
"You are running in webmux ONESHOT mode. There is NO interactive user \u2014 nobody is watching the chat or will respond to questions, approvals, or status checks. Any message asking the user to review, approve, confirm, take a look, or 'let you know' is wasted output: it will not be answered.",
|
|
13004
|
+
"Your job is to take the task to its real conclusion without pausing:",
|
|
13005
|
+
"1) Make the change. 2) Validate it (run the relevant tests, typecheck, build, or quick manual check). 3) Commit. 4) Push. 5) Open a pull request. Only then are you done.",
|
|
13006
|
+
"When something is ambiguous, pick the most reasonable default and proceed. When you would normally ask 'should I X or Y?', just pick one and continue \u2014 note the choice in the PR description if it matters.",
|
|
13007
|
+
"Never end your turn with a question, a suggestion to 'take a look', or a request for approval. Stop only when the PR is open, or when you hit a technical error you cannot recover from yourself (in which case clearly state the blocker)."
|
|
13008
|
+
].join(" ");
|
|
13009
|
+
}
|
|
12827
13010
|
var DEFAULT_CONFIG = {
|
|
12828
13011
|
name: "Webmux",
|
|
12829
13012
|
workspace: {
|
|
@@ -12847,7 +13030,8 @@ var DEFAULT_CONFIG = {
|
|
|
12847
13030
|
linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
|
|
12848
13031
|
},
|
|
12849
13032
|
lifecycleHooks: {},
|
|
12850
|
-
autoName: null
|
|
13033
|
+
autoName: null,
|
|
13034
|
+
oneshot: { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() }
|
|
12851
13035
|
};
|
|
12852
13036
|
function clonePanes(panes) {
|
|
12853
13037
|
return panes.map((pane) => ({ ...pane }));
|
|
@@ -13025,6 +13209,12 @@ function parseLifecycleHooks(raw) {
|
|
|
13025
13209
|
}
|
|
13026
13210
|
return hooks;
|
|
13027
13211
|
}
|
|
13212
|
+
function parseOneshot(raw) {
|
|
13213
|
+
if (!isRecord3(raw))
|
|
13214
|
+
return { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() };
|
|
13215
|
+
const systemPrompt = typeof raw.systemPrompt === "string" && raw.systemPrompt.trim() ? raw.systemPrompt.trim() : DEFAULT_ONESHOT_SYSTEM_PROMPT();
|
|
13216
|
+
return { systemPrompt };
|
|
13217
|
+
}
|
|
13028
13218
|
function parseAutoName(raw) {
|
|
13029
13219
|
if (!isRecord3(raw))
|
|
13030
13220
|
return null;
|
|
@@ -13097,7 +13287,8 @@ function parseProjectConfig(parsed) {
|
|
|
13097
13287
|
}
|
|
13098
13288
|
},
|
|
13099
13289
|
lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
|
|
13100
|
-
autoName: parseAutoName(parsed.auto_name)
|
|
13290
|
+
autoName: parseAutoName(parsed.auto_name),
|
|
13291
|
+
oneshot: parseOneshot(parsed.oneshot)
|
|
13101
13292
|
};
|
|
13102
13293
|
}
|
|
13103
13294
|
function defaultConfig() {
|
|
@@ -13603,6 +13794,46 @@ function validateCustomAgentInput(input) {
|
|
|
13603
13794
|
}
|
|
13604
13795
|
|
|
13605
13796
|
// backend/src/services/linear-service.ts
|
|
13797
|
+
import { request as httpsRequest } from "https";
|
|
13798
|
+
function getHeader(headers, name) {
|
|
13799
|
+
const lower = name.toLowerCase();
|
|
13800
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
13801
|
+
if (key.toLowerCase() === lower)
|
|
13802
|
+
return value;
|
|
13803
|
+
}
|
|
13804
|
+
return;
|
|
13805
|
+
}
|
|
13806
|
+
function hasHeader(headers, name) {
|
|
13807
|
+
return getHeader(headers, name) !== undefined;
|
|
13808
|
+
}
|
|
13809
|
+
function putViaNodeHttps(url, headers, body) {
|
|
13810
|
+
return new Promise((resolve3, reject) => {
|
|
13811
|
+
const parsed = new URL(url);
|
|
13812
|
+
const req = httpsRequest({
|
|
13813
|
+
method: "PUT",
|
|
13814
|
+
hostname: parsed.hostname,
|
|
13815
|
+
port: parsed.port || 443,
|
|
13816
|
+
path: parsed.pathname + parsed.search,
|
|
13817
|
+
headers: {
|
|
13818
|
+
...headers,
|
|
13819
|
+
"Content-Length": String(body.byteLength)
|
|
13820
|
+
}
|
|
13821
|
+
}, (res) => {
|
|
13822
|
+
const chunks = [];
|
|
13823
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
13824
|
+
res.on("end", () => {
|
|
13825
|
+
resolve3({
|
|
13826
|
+
status: res.statusCode ?? 0,
|
|
13827
|
+
body: Buffer.concat(chunks).toString("utf8")
|
|
13828
|
+
});
|
|
13829
|
+
});
|
|
13830
|
+
res.on("error", reject);
|
|
13831
|
+
});
|
|
13832
|
+
req.on("error", reject);
|
|
13833
|
+
req.write(Buffer.from(body));
|
|
13834
|
+
req.end();
|
|
13835
|
+
});
|
|
13836
|
+
}
|
|
13606
13837
|
var ASSIGNED_ISSUES_QUERY = `
|
|
13607
13838
|
query AssignedIssues {
|
|
13608
13839
|
viewer {
|
|
@@ -13897,6 +14128,278 @@ async function fetchAssignedIssues(options) {
|
|
|
13897
14128
|
}
|
|
13898
14129
|
return result;
|
|
13899
14130
|
}
|
|
14131
|
+
var ISSUE_WITH_ATTACHMENTS_QUERY = `
|
|
14132
|
+
query IssueWithAttachments($id: String!) {
|
|
14133
|
+
issue(id: $id) {
|
|
14134
|
+
id
|
|
14135
|
+
identifier
|
|
14136
|
+
title
|
|
14137
|
+
description
|
|
14138
|
+
url
|
|
14139
|
+
branchName
|
|
14140
|
+
attachments {
|
|
14141
|
+
nodes {
|
|
14142
|
+
id
|
|
14143
|
+
url
|
|
14144
|
+
title
|
|
14145
|
+
subtitle
|
|
14146
|
+
sourceType
|
|
14147
|
+
metadata
|
|
14148
|
+
createdAt
|
|
14149
|
+
}
|
|
14150
|
+
}
|
|
14151
|
+
}
|
|
14152
|
+
}
|
|
14153
|
+
`;
|
|
14154
|
+
var TEAM_BY_KEY_QUERY = `
|
|
14155
|
+
query TeamByKey($key: String!) {
|
|
14156
|
+
teams(filter: { key: { eq: $key } }, first: 1) {
|
|
14157
|
+
nodes {
|
|
14158
|
+
id
|
|
14159
|
+
key
|
|
14160
|
+
name
|
|
14161
|
+
}
|
|
14162
|
+
}
|
|
14163
|
+
}
|
|
14164
|
+
`;
|
|
14165
|
+
var FILE_UPLOAD_MUTATION = `
|
|
14166
|
+
mutation FileUpload($contentType: String!, $filename: String!, $size: Int!) {
|
|
14167
|
+
fileUpload(contentType: $contentType, filename: $filename, size: $size) {
|
|
14168
|
+
success
|
|
14169
|
+
uploadFile {
|
|
14170
|
+
uploadUrl
|
|
14171
|
+
assetUrl
|
|
14172
|
+
headers {
|
|
14173
|
+
key
|
|
14174
|
+
value
|
|
14175
|
+
}
|
|
14176
|
+
}
|
|
14177
|
+
}
|
|
14178
|
+
}
|
|
14179
|
+
`;
|
|
14180
|
+
var ATTACHMENT_CREATE_MUTATION = `
|
|
14181
|
+
mutation AttachmentCreate($issueId: String!, $title: String!, $url: String!, $subtitle: String) {
|
|
14182
|
+
attachmentCreate(input: { issueId: $issueId, title: $title, url: $url, subtitle: $subtitle }) {
|
|
14183
|
+
success
|
|
14184
|
+
attachment {
|
|
14185
|
+
id
|
|
14186
|
+
url
|
|
14187
|
+
}
|
|
14188
|
+
}
|
|
14189
|
+
}
|
|
14190
|
+
`;
|
|
14191
|
+
var COMMENT_CREATE_MUTATION = `
|
|
14192
|
+
mutation CommentCreate($issueId: String!, $body: String!) {
|
|
14193
|
+
commentCreate(input: { issueId: $issueId, body: $body }) {
|
|
14194
|
+
success
|
|
14195
|
+
comment {
|
|
14196
|
+
id
|
|
14197
|
+
url
|
|
14198
|
+
}
|
|
14199
|
+
}
|
|
14200
|
+
}
|
|
14201
|
+
`;
|
|
14202
|
+
var WEBMUX_ATTACHMENT_TITLE_PREFIX = "webmux-state:";
|
|
14203
|
+
function buildWebmuxAttachmentTitle(branch) {
|
|
14204
|
+
return `${WEBMUX_ATTACHMENT_TITLE_PREFIX}${branch}`;
|
|
14205
|
+
}
|
|
14206
|
+
function findWebmuxAttachment(issue, branch) {
|
|
14207
|
+
const candidates = issue.attachments.filter((a) => a.title.startsWith(WEBMUX_ATTACHMENT_TITLE_PREFIX));
|
|
14208
|
+
if (candidates.length === 0)
|
|
14209
|
+
return null;
|
|
14210
|
+
if (branch) {
|
|
14211
|
+
const exact = candidates.find((a) => a.title === buildWebmuxAttachmentTitle(branch));
|
|
14212
|
+
if (exact)
|
|
14213
|
+
return exact;
|
|
14214
|
+
}
|
|
14215
|
+
return [...candidates].sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] ?? null;
|
|
14216
|
+
}
|
|
14217
|
+
function inferPrStateFromAttachment(attachment) {
|
|
14218
|
+
const meta = attachment.metadata ?? {};
|
|
14219
|
+
const rawState = typeof meta.state === "string" ? meta.state.toLowerCase() : null;
|
|
14220
|
+
if (rawState === "open" || rawState === "closed" || rawState === "merged")
|
|
14221
|
+
return rawState;
|
|
14222
|
+
const status = typeof meta.status === "string" ? meta.status.toLowerCase() : null;
|
|
14223
|
+
if (status === "open" || status === "closed" || status === "merged")
|
|
14224
|
+
return status;
|
|
14225
|
+
return "unknown";
|
|
14226
|
+
}
|
|
14227
|
+
function inferPrBranchFromAttachment(attachment) {
|
|
14228
|
+
const meta = attachment.metadata ?? {};
|
|
14229
|
+
if (typeof meta.branchName === "string" && meta.branchName.trim())
|
|
14230
|
+
return meta.branchName.trim();
|
|
14231
|
+
if (typeof meta.headRefName === "string" && meta.headRefName.trim())
|
|
14232
|
+
return meta.headRefName.trim();
|
|
14233
|
+
return null;
|
|
14234
|
+
}
|
|
14235
|
+
var STATE_PRIORITY = {
|
|
14236
|
+
open: 0,
|
|
14237
|
+
merged: 1,
|
|
14238
|
+
closed: 2,
|
|
14239
|
+
unknown: 3
|
|
14240
|
+
};
|
|
14241
|
+
function findLinkedGitHubPr(issue) {
|
|
14242
|
+
const githubAttachments = issue.attachments.filter((a) => {
|
|
14243
|
+
if (a.sourceType === "github" || a.sourceType === "githubPR" || a.sourceType === "github_pull_request") {
|
|
14244
|
+
return true;
|
|
14245
|
+
}
|
|
14246
|
+
return /github\.com\/.+\/pull\/\d+/i.test(a.url);
|
|
14247
|
+
});
|
|
14248
|
+
if (githubAttachments.length === 0)
|
|
14249
|
+
return null;
|
|
14250
|
+
const prs = githubAttachments.map((a) => ({
|
|
14251
|
+
url: a.url,
|
|
14252
|
+
branch: inferPrBranchFromAttachment(a),
|
|
14253
|
+
state: inferPrStateFromAttachment(a)
|
|
14254
|
+
}));
|
|
14255
|
+
const indexed = prs.map((pr, idx) => ({ pr, idx, attachment: githubAttachments[idx] }));
|
|
14256
|
+
indexed.sort((a, b) => {
|
|
14257
|
+
const stateDiff = STATE_PRIORITY[a.pr.state] - STATE_PRIORITY[b.pr.state];
|
|
14258
|
+
if (stateDiff !== 0)
|
|
14259
|
+
return stateDiff;
|
|
14260
|
+
return b.attachment.createdAt.localeCompare(a.attachment.createdAt);
|
|
14261
|
+
});
|
|
14262
|
+
return indexed[0].pr;
|
|
14263
|
+
}
|
|
14264
|
+
function buildLinearSummaryMarkdown(input) {
|
|
14265
|
+
const lines = [
|
|
14266
|
+
`**Webmux session \u2014 branch \`${input.branch}\`**`,
|
|
14267
|
+
""
|
|
14268
|
+
];
|
|
14269
|
+
if (input.baseBranch)
|
|
14270
|
+
lines.push(`- Base: \`${input.baseBranch}\``);
|
|
14271
|
+
lines.push(`- Turns: ${input.turns}`);
|
|
14272
|
+
if (input.prUrl)
|
|
14273
|
+
lines.push(`- PR: ${input.prUrl}`);
|
|
14274
|
+
lines.push(`- Transcript: see attachment \`${input.attachmentTitle}\``);
|
|
14275
|
+
if (input.webmuxVersion)
|
|
14276
|
+
lines.push(`- webmux: ${input.webmuxVersion}`);
|
|
14277
|
+
lines.push("");
|
|
14278
|
+
lines.push("_Resume on another machine with_ `webmux oneshot --linear <issue-id>`.");
|
|
14279
|
+
return lines.join(`
|
|
14280
|
+
`);
|
|
14281
|
+
}
|
|
14282
|
+
async function fetchIssueWithAttachments(issueIdentifierOrId) {
|
|
14283
|
+
const response = await postLinearGraphql(ISSUE_WITH_ATTACHMENTS_QUERY, {
|
|
14284
|
+
id: issueIdentifierOrId
|
|
14285
|
+
});
|
|
14286
|
+
if (!response.ok) {
|
|
14287
|
+
return { ok: false, error: response.error, status: 502 };
|
|
14288
|
+
}
|
|
14289
|
+
const error = gqlErrorMessage(response.data);
|
|
14290
|
+
if (error) {
|
|
14291
|
+
return { ok: false, error, status: 502 };
|
|
14292
|
+
}
|
|
14293
|
+
const issue = response.data.data?.issue;
|
|
14294
|
+
if (!issue) {
|
|
14295
|
+
return { ok: false, error: `Linear issue not found: ${issueIdentifierOrId}`, status: 404 };
|
|
14296
|
+
}
|
|
14297
|
+
return {
|
|
14298
|
+
ok: true,
|
|
14299
|
+
data: {
|
|
14300
|
+
id: issue.id,
|
|
14301
|
+
identifier: issue.identifier,
|
|
14302
|
+
title: issue.title,
|
|
14303
|
+
description: issue.description,
|
|
14304
|
+
url: issue.url,
|
|
14305
|
+
branchName: issue.branchName,
|
|
14306
|
+
attachments: issue.attachments.nodes.map((node) => ({
|
|
14307
|
+
id: node.id,
|
|
14308
|
+
url: node.url,
|
|
14309
|
+
title: node.title,
|
|
14310
|
+
subtitle: node.subtitle,
|
|
14311
|
+
sourceType: node.sourceType,
|
|
14312
|
+
metadata: node.metadata,
|
|
14313
|
+
createdAt: node.createdAt
|
|
14314
|
+
}))
|
|
14315
|
+
}
|
|
14316
|
+
};
|
|
14317
|
+
}
|
|
14318
|
+
async function fetchTeamByKey(teamKey) {
|
|
14319
|
+
const response = await postLinearGraphql(TEAM_BY_KEY_QUERY, { key: teamKey });
|
|
14320
|
+
if (!response.ok) {
|
|
14321
|
+
return { ok: false, error: response.error, status: 502 };
|
|
14322
|
+
}
|
|
14323
|
+
const error = gqlErrorMessage(response.data);
|
|
14324
|
+
if (error) {
|
|
14325
|
+
return { ok: false, error, status: 502 };
|
|
14326
|
+
}
|
|
14327
|
+
const team = response.data.data?.teams.nodes[0];
|
|
14328
|
+
if (!team) {
|
|
14329
|
+
return { ok: false, error: `Linear team not found for key: ${teamKey}`, status: 404 };
|
|
14330
|
+
}
|
|
14331
|
+
return { ok: true, data: team };
|
|
14332
|
+
}
|
|
14333
|
+
async function uploadAttachmentFile(input) {
|
|
14334
|
+
const response = await postLinearGraphql(FILE_UPLOAD_MUTATION, {
|
|
14335
|
+
contentType: input.contentType,
|
|
14336
|
+
filename: input.filename,
|
|
14337
|
+
size: input.body.byteLength
|
|
14338
|
+
});
|
|
14339
|
+
if (!response.ok)
|
|
14340
|
+
return { ok: false, error: response.error };
|
|
14341
|
+
const error = gqlErrorMessage(response.data);
|
|
14342
|
+
if (error)
|
|
14343
|
+
return { ok: false, error };
|
|
14344
|
+
const upload = response.data.data?.fileUpload;
|
|
14345
|
+
if (!upload?.success || !upload.uploadFile) {
|
|
14346
|
+
return { ok: false, error: "Linear fileUpload did not return an upload URL" };
|
|
14347
|
+
}
|
|
14348
|
+
const headers = {};
|
|
14349
|
+
for (const h of upload.uploadFile.headers)
|
|
14350
|
+
headers[h.key] = h.value;
|
|
14351
|
+
if (!hasHeader(headers, "content-type")) {
|
|
14352
|
+
headers["Content-Type"] = input.contentType;
|
|
14353
|
+
}
|
|
14354
|
+
if (!hasHeader(headers, "x-goog-content-length-range")) {
|
|
14355
|
+
const size = input.body.byteLength;
|
|
14356
|
+
headers["x-goog-content-length-range"] = `${size},${size}`;
|
|
14357
|
+
}
|
|
14358
|
+
try {
|
|
14359
|
+
const { status, body } = await putViaNodeHttps(upload.uploadFile.uploadUrl, headers, input.body);
|
|
14360
|
+
if (status < 200 || status >= 300) {
|
|
14361
|
+
return { ok: false, error: `Asset upload failed ${status}: ${body.slice(0, 1000)}` };
|
|
14362
|
+
}
|
|
14363
|
+
} catch (err) {
|
|
14364
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14365
|
+
return { ok: false, error: `Asset upload error: ${msg}` };
|
|
14366
|
+
}
|
|
14367
|
+
return { ok: true, data: { assetUrl: upload.uploadFile.assetUrl } };
|
|
14368
|
+
}
|
|
14369
|
+
async function attachToIssue(input) {
|
|
14370
|
+
const response = await postLinearGraphql(ATTACHMENT_CREATE_MUTATION, {
|
|
14371
|
+
issueId: input.issueId,
|
|
14372
|
+
title: input.title,
|
|
14373
|
+
url: input.url,
|
|
14374
|
+
subtitle: input.subtitle ?? null
|
|
14375
|
+
});
|
|
14376
|
+
if (!response.ok)
|
|
14377
|
+
return { ok: false, error: response.error };
|
|
14378
|
+
const error = gqlErrorMessage(response.data);
|
|
14379
|
+
if (error)
|
|
14380
|
+
return { ok: false, error };
|
|
14381
|
+
const payload = response.data.data?.attachmentCreate;
|
|
14382
|
+
if (!payload?.success || !payload.attachment) {
|
|
14383
|
+
return { ok: false, error: "Linear attachmentCreate did not succeed" };
|
|
14384
|
+
}
|
|
14385
|
+
return { ok: true, data: payload.attachment };
|
|
14386
|
+
}
|
|
14387
|
+
async function createIssueComment(input) {
|
|
14388
|
+
const response = await postLinearGraphql(COMMENT_CREATE_MUTATION, {
|
|
14389
|
+
issueId: input.issueId,
|
|
14390
|
+
body: input.body
|
|
14391
|
+
});
|
|
14392
|
+
if (!response.ok)
|
|
14393
|
+
return { ok: false, error: response.error };
|
|
14394
|
+
const error = gqlErrorMessage(response.data);
|
|
14395
|
+
if (error)
|
|
14396
|
+
return { ok: false, error };
|
|
14397
|
+
const payload = response.data.data?.commentCreate;
|
|
14398
|
+
if (!payload?.success || !payload.comment) {
|
|
14399
|
+
return { ok: false, error: "Linear commentCreate did not succeed" };
|
|
14400
|
+
}
|
|
14401
|
+
return { ok: true, data: payload.comment };
|
|
14402
|
+
}
|
|
13900
14403
|
async function createLinearIssue(input) {
|
|
13901
14404
|
const viewerResult = await fetchViewerId();
|
|
13902
14405
|
if (!viewerResult.ok) {
|
|
@@ -13929,6 +14432,202 @@ async function createLinearIssue(input) {
|
|
|
13929
14432
|
return result;
|
|
13930
14433
|
}
|
|
13931
14434
|
|
|
14435
|
+
// backend/src/services/conversation-export-service.ts
|
|
14436
|
+
var WebmuxConversationAttachmentPayloadSchema = exports_external.object({
|
|
14437
|
+
webmux: exports_external.literal(1),
|
|
14438
|
+
branch: exports_external.string(),
|
|
14439
|
+
baseBranch: exports_external.string().nullable(),
|
|
14440
|
+
agent: AgentIdSchema.nullable(),
|
|
14441
|
+
createdAt: exports_external.string(),
|
|
14442
|
+
conversation: exports_external.array(AgentsUiConversationMessageSchema)
|
|
14443
|
+
});
|
|
14444
|
+
var defaultSeedFromLinearDeps = {
|
|
14445
|
+
fetchIssueWithAttachments,
|
|
14446
|
+
downloadWebmuxAttachment: downloadWebmuxAttachmentDefault
|
|
14447
|
+
};
|
|
14448
|
+
function countConversationTurns(conversation) {
|
|
14449
|
+
return new Set(conversation.messages.map((m) => m.turnId)).size;
|
|
14450
|
+
}
|
|
14451
|
+
function escapeFence(text) {
|
|
14452
|
+
return text.replace(/```/g, "``\u200B`");
|
|
14453
|
+
}
|
|
14454
|
+
function buildConversationAttachmentPayload(input) {
|
|
14455
|
+
const now = input.now ?? (() => new Date);
|
|
14456
|
+
return {
|
|
14457
|
+
webmux: 1,
|
|
14458
|
+
branch: input.branch,
|
|
14459
|
+
baseBranch: input.baseBranch,
|
|
14460
|
+
agent: input.agent,
|
|
14461
|
+
createdAt: now().toISOString(),
|
|
14462
|
+
conversation: input.conversation.messages
|
|
14463
|
+
};
|
|
14464
|
+
}
|
|
14465
|
+
async function resolveIssue(input, deps) {
|
|
14466
|
+
if (input.target.kind === "issue") {
|
|
14467
|
+
const issue = await deps.fetchIssueWithAttachments(input.target.issueId);
|
|
14468
|
+
if (!issue.ok)
|
|
14469
|
+
return issue;
|
|
14470
|
+
return { ok: true, issueId: issue.data.id, issueUrl: issue.data.url };
|
|
14471
|
+
}
|
|
14472
|
+
const team = await deps.fetchTeamByKey(input.target.teamKey);
|
|
14473
|
+
if (!team.ok)
|
|
14474
|
+
return team;
|
|
14475
|
+
const titleFromPrompt = input.target.title?.trim();
|
|
14476
|
+
const title = titleFromPrompt && titleFromPrompt.length > 0 ? titleFromPrompt : `Webmux session: ${input.branch}`;
|
|
14477
|
+
const description = [
|
|
14478
|
+
`Created from a webmux session on branch \`${input.branch}\`.`,
|
|
14479
|
+
input.prUrl ? `
|
|
14480
|
+
PR: ${input.prUrl}` : ""
|
|
14481
|
+
].filter(Boolean).join(`
|
|
14482
|
+
`);
|
|
14483
|
+
const created = await deps.createLinearIssue({
|
|
14484
|
+
teamId: team.data.id,
|
|
14485
|
+
title,
|
|
14486
|
+
description
|
|
14487
|
+
});
|
|
14488
|
+
if (!created.ok)
|
|
14489
|
+
return { ok: false, error: created.error, status: 502 };
|
|
14490
|
+
return { ok: true, issueId: created.data.id, issueUrl: created.data.url };
|
|
14491
|
+
}
|
|
14492
|
+
async function exportConversationToLinear(input, deps) {
|
|
14493
|
+
const issue = await resolveIssue(input, deps);
|
|
14494
|
+
if (!issue.ok)
|
|
14495
|
+
return issue;
|
|
14496
|
+
const payload = buildConversationAttachmentPayload(input);
|
|
14497
|
+
const attachmentTitle = buildWebmuxAttachmentTitle(input.branch);
|
|
14498
|
+
const bodyBytes = new TextEncoder().encode(JSON.stringify(payload, null, 2));
|
|
14499
|
+
const filename = `${attachmentTitle}.json`;
|
|
14500
|
+
const upload = await deps.uploadAttachmentFile({
|
|
14501
|
+
filename,
|
|
14502
|
+
contentType: "application/json",
|
|
14503
|
+
body: bodyBytes.buffer
|
|
14504
|
+
});
|
|
14505
|
+
if (!upload.ok) {
|
|
14506
|
+
return { ok: false, error: `Linear file upload failed: ${upload.error}`, status: 502 };
|
|
14507
|
+
}
|
|
14508
|
+
const attached = await deps.attachToIssue({
|
|
14509
|
+
issueId: issue.issueId,
|
|
14510
|
+
title: attachmentTitle,
|
|
14511
|
+
url: upload.data.assetUrl,
|
|
14512
|
+
subtitle: input.prUrl ?? undefined
|
|
14513
|
+
});
|
|
14514
|
+
if (!attached.ok) {
|
|
14515
|
+
return { ok: false, error: `Linear attachmentCreate failed: ${attached.error}`, status: 502 };
|
|
14516
|
+
}
|
|
14517
|
+
const summary = buildLinearSummaryMarkdown({
|
|
14518
|
+
branch: input.branch,
|
|
14519
|
+
baseBranch: input.baseBranch ?? undefined,
|
|
14520
|
+
turns: countConversationTurns(input.conversation),
|
|
14521
|
+
prUrl: input.prUrl ?? undefined,
|
|
14522
|
+
attachmentTitle,
|
|
14523
|
+
webmuxVersion: input.webmuxVersion
|
|
14524
|
+
});
|
|
14525
|
+
const comment = await deps.createIssueComment({
|
|
14526
|
+
issueId: issue.issueId,
|
|
14527
|
+
body: summary
|
|
14528
|
+
});
|
|
14529
|
+
let commentUrl = null;
|
|
14530
|
+
if (comment.ok) {
|
|
14531
|
+
commentUrl = comment.data.url;
|
|
14532
|
+
} else {
|
|
14533
|
+
log.error(`[linear] comment creation failed (attachment still saved): ${comment.error}`);
|
|
14534
|
+
}
|
|
14535
|
+
return {
|
|
14536
|
+
ok: true,
|
|
14537
|
+
data: {
|
|
14538
|
+
issueId: issue.issueId,
|
|
14539
|
+
issueUrl: issue.issueUrl,
|
|
14540
|
+
attachmentUrl: upload.data.assetUrl,
|
|
14541
|
+
commentUrl
|
|
14542
|
+
}
|
|
14543
|
+
};
|
|
14544
|
+
}
|
|
14545
|
+
function buildIssueHeader(issue) {
|
|
14546
|
+
const lines = [];
|
|
14547
|
+
lines.push(`This worktree is for Linear issue **${issue.identifier}** \u2014 ${issue.url}`);
|
|
14548
|
+
lines.push("");
|
|
14549
|
+
lines.push(`When opening a PR, reference \`Fixes ${issue.identifier}\` in the title or body so Linear links it back automatically (Linear also auto-links PRs on the branch \`${issue.branchName}\`).`);
|
|
14550
|
+
lines.push("");
|
|
14551
|
+
lines.push(`## Issue: ${issue.title}`);
|
|
14552
|
+
if (issue.description?.trim()) {
|
|
14553
|
+
lines.push("");
|
|
14554
|
+
lines.push(escapeFence(issue.description.trim()));
|
|
14555
|
+
}
|
|
14556
|
+
lines.push("");
|
|
14557
|
+
return lines.join(`
|
|
14558
|
+
`);
|
|
14559
|
+
}
|
|
14560
|
+
function buildPriorConversationSection(payload) {
|
|
14561
|
+
const lines = [];
|
|
14562
|
+
lines.push(`---`);
|
|
14563
|
+
lines.push("");
|
|
14564
|
+
lines.push(`A previous webmux session for this issue was saved here (branch \`${payload.branch}\`${payload.baseBranch ? `, base \`${payload.baseBranch}\`` : ""}).`);
|
|
14565
|
+
lines.push("");
|
|
14566
|
+
lines.push("Previous conversation (chronological):");
|
|
14567
|
+
lines.push("");
|
|
14568
|
+
for (const message of payload.conversation) {
|
|
14569
|
+
lines.push(`### ${message.role}`);
|
|
14570
|
+
lines.push("");
|
|
14571
|
+
lines.push(escapeFence(message.text));
|
|
14572
|
+
lines.push("");
|
|
14573
|
+
}
|
|
14574
|
+
return lines.join(`
|
|
14575
|
+
`);
|
|
14576
|
+
}
|
|
14577
|
+
async function buildSeedFromLinear(input, deps) {
|
|
14578
|
+
const issue = await deps.fetchIssueWithAttachments(input.issueId);
|
|
14579
|
+
if (!issue.ok)
|
|
14580
|
+
return issue;
|
|
14581
|
+
const issueHeader = buildIssueHeader(issue.data);
|
|
14582
|
+
const webmuxAttachment = findWebmuxAttachment(issue.data, input.preferBranch);
|
|
14583
|
+
const pr = findLinkedGitHubPr(issue.data);
|
|
14584
|
+
let attachmentPayload = null;
|
|
14585
|
+
if (webmuxAttachment) {
|
|
14586
|
+
const payloadResult = await deps.downloadWebmuxAttachment(webmuxAttachment.url);
|
|
14587
|
+
if (payloadResult.ok) {
|
|
14588
|
+
attachmentPayload = payloadResult.data;
|
|
14589
|
+
} else {
|
|
14590
|
+
log.error(`[linear] webmux attachment download failed: ${payloadResult.error}`);
|
|
14591
|
+
}
|
|
14592
|
+
}
|
|
14593
|
+
const source = attachmentPayload ? "webmux-attachment" : pr ? "github-integration" : "none";
|
|
14594
|
+
const branch = attachmentPayload?.branch ?? pr?.branch ?? (issue.data.branchName || null);
|
|
14595
|
+
const baseBranch = attachmentPayload?.baseBranch ?? null;
|
|
14596
|
+
const conversationMarkdown = attachmentPayload ? `${issueHeader}${buildPriorConversationSection(attachmentPayload)}` : issueHeader;
|
|
14597
|
+
return {
|
|
14598
|
+
ok: true,
|
|
14599
|
+
data: {
|
|
14600
|
+
source,
|
|
14601
|
+
branch,
|
|
14602
|
+
baseBranch,
|
|
14603
|
+
prUrl: pr?.url ?? null,
|
|
14604
|
+
conversationMarkdown
|
|
14605
|
+
}
|
|
14606
|
+
};
|
|
14607
|
+
}
|
|
14608
|
+
async function downloadWebmuxAttachmentDefault(url) {
|
|
14609
|
+
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
14610
|
+
if (!apiKey)
|
|
14611
|
+
return { ok: false, error: "LINEAR_API_KEY not set" };
|
|
14612
|
+
try {
|
|
14613
|
+
const res = await fetch(url, {
|
|
14614
|
+
headers: { Authorization: apiKey }
|
|
14615
|
+
});
|
|
14616
|
+
if (!res.ok) {
|
|
14617
|
+
return { ok: false, error: `Asset download failed ${res.status}` };
|
|
14618
|
+
}
|
|
14619
|
+
const text = await res.text();
|
|
14620
|
+
const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(JSON.parse(text));
|
|
14621
|
+
if (!parsed.success) {
|
|
14622
|
+
return { ok: false, error: "Asset is not a webmux conversation payload" };
|
|
14623
|
+
}
|
|
14624
|
+
return { ok: true, data: parsed.data };
|
|
14625
|
+
} catch (err) {
|
|
14626
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14627
|
+
return { ok: false, error: msg };
|
|
14628
|
+
}
|
|
14629
|
+
}
|
|
14630
|
+
|
|
13932
14631
|
// backend/src/services/lifecycle-service.ts
|
|
13933
14632
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
13934
14633
|
import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
@@ -14797,23 +15496,22 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
|
14797
15496
|
return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
|
|
14798
15497
|
}
|
|
14799
15498
|
function buildBuiltInAgentInvocation(input) {
|
|
15499
|
+
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
14800
15500
|
if (input.agent === "codex") {
|
|
14801
15501
|
const hooksFlag = " --enable codex_hooks";
|
|
14802
15502
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
14803
15503
|
if (input.launchMode === "resume") {
|
|
14804
|
-
return `codex${hooksFlag}${yoloFlag2} resume --last`;
|
|
15504
|
+
return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
|
|
14805
15505
|
}
|
|
14806
|
-
const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
14807
15506
|
if (input.systemPrompt) {
|
|
14808
|
-
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${
|
|
15507
|
+
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
|
|
14809
15508
|
}
|
|
14810
|
-
return `codex${hooksFlag}${yoloFlag2}${
|
|
15509
|
+
return `codex${hooksFlag}${yoloFlag2}${promptSuffix}`;
|
|
14811
15510
|
}
|
|
14812
15511
|
const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
|
|
14813
15512
|
if (input.launchMode === "resume") {
|
|
14814
|
-
return `claude${yoloFlag} --continue`;
|
|
15513
|
+
return `claude${yoloFlag} --continue${promptSuffix}`;
|
|
14815
15514
|
}
|
|
14816
|
-
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
14817
15515
|
if (input.systemPrompt) {
|
|
14818
15516
|
return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
|
|
14819
15517
|
}
|
|
@@ -15353,7 +16051,9 @@ async function initializeManagedWorktree(opts) {
|
|
|
15353
16051
|
agent: opts.agent,
|
|
15354
16052
|
runtime: opts.runtime,
|
|
15355
16053
|
startupEnvValues: { ...opts.startupEnvValues ?? {} },
|
|
15356
|
-
allocatedPorts: { ...opts.allocatedPorts ?? {} }
|
|
16054
|
+
allocatedPorts: { ...opts.allocatedPorts ?? {} },
|
|
16055
|
+
...opts.source ? { source: opts.source } : {},
|
|
16056
|
+
...opts.oneshot ? { oneshot: opts.oneshot } : {}
|
|
15357
16057
|
};
|
|
15358
16058
|
const paths = await ensureWorktreeStorageDirs(opts.gitDir);
|
|
15359
16059
|
await writeWorktreeMeta(opts.gitDir, meta);
|
|
@@ -15406,7 +16106,9 @@ async function createManagedWorktree(opts, deps = {}) {
|
|
|
15406
16106
|
controlUrl: opts.controlUrl,
|
|
15407
16107
|
controlToken: opts.controlToken,
|
|
15408
16108
|
now: opts.now,
|
|
15409
|
-
worktreeId: opts.worktreeId
|
|
16109
|
+
worktreeId: opts.worktreeId,
|
|
16110
|
+
...opts.source ? { source: opts.source } : {},
|
|
16111
|
+
...opts.oneshot ? { oneshot: opts.oneshot } : {}
|
|
15410
16112
|
});
|
|
15411
16113
|
if (deps.tmux) {
|
|
15412
16114
|
sessionLayoutPlan = sessionLayoutPlan ?? opts.sessionLayoutPlanBuilder?.(initialized);
|
|
@@ -15555,10 +16257,15 @@ class LifecycleService {
|
|
|
15555
16257
|
agent: agent.id
|
|
15556
16258
|
});
|
|
15557
16259
|
}
|
|
15558
|
-
async openWorktree(branch) {
|
|
16260
|
+
async openWorktree(branch, options = {}) {
|
|
15559
16261
|
try {
|
|
15560
16262
|
const resolved = await this.resolveExistingWorktree(branch);
|
|
15561
|
-
|
|
16263
|
+
let initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
|
|
16264
|
+
if (options.oneshot) {
|
|
16265
|
+
const nextMeta = { ...initialized.meta, oneshot: options.oneshot };
|
|
16266
|
+
await writeWorktreeMeta(initialized.paths.gitDir, nextMeta);
|
|
16267
|
+
initialized = { ...initialized, meta: nextMeta };
|
|
16268
|
+
}
|
|
15562
16269
|
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
15563
16270
|
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
15564
16271
|
const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
|
|
@@ -15573,7 +16280,8 @@ class LifecycleService {
|
|
|
15573
16280
|
agent,
|
|
15574
16281
|
initialized,
|
|
15575
16282
|
worktreePath: resolved.entry.path,
|
|
15576
|
-
launchMode
|
|
16283
|
+
launchMode,
|
|
16284
|
+
followUpPrompt: options.prompt
|
|
15577
16285
|
});
|
|
15578
16286
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
15579
16287
|
return {
|
|
@@ -15584,6 +16292,20 @@ class LifecycleService {
|
|
|
15584
16292
|
throw this.wrapOperationError(error);
|
|
15585
16293
|
}
|
|
15586
16294
|
}
|
|
16295
|
+
async disarmOneshot(branch) {
|
|
16296
|
+
let resolved;
|
|
16297
|
+
try {
|
|
16298
|
+
resolved = await this.resolveExistingWorktree(branch);
|
|
16299
|
+
} catch {
|
|
16300
|
+
return false;
|
|
16301
|
+
}
|
|
16302
|
+
if (!resolved.meta?.oneshot)
|
|
16303
|
+
return false;
|
|
16304
|
+
const nextMeta = { ...resolved.meta };
|
|
16305
|
+
delete nextMeta.oneshot;
|
|
16306
|
+
await writeWorktreeMeta(resolved.gitDir, nextMeta);
|
|
16307
|
+
return true;
|
|
16308
|
+
}
|
|
15587
16309
|
async closeWorktree(branch) {
|
|
15588
16310
|
try {
|
|
15589
16311
|
await this.resolveExistingWorktree(branch);
|
|
@@ -15870,8 +16592,10 @@ class LifecycleService {
|
|
|
15870
16592
|
agent: input.agent,
|
|
15871
16593
|
initialized: input.initialized,
|
|
15872
16594
|
worktreePath: input.worktreePath,
|
|
15873
|
-
|
|
16595
|
+
creationPrompt: input.creationPrompt,
|
|
16596
|
+
followUpPrompt: input.followUpPrompt,
|
|
15874
16597
|
launchMode: input.launchMode,
|
|
16598
|
+
source: input.source,
|
|
15875
16599
|
containerName
|
|
15876
16600
|
}));
|
|
15877
16601
|
return;
|
|
@@ -15883,12 +16607,19 @@ class LifecycleService {
|
|
|
15883
16607
|
agent: input.agent,
|
|
15884
16608
|
initialized: input.initialized,
|
|
15885
16609
|
worktreePath: input.worktreePath,
|
|
15886
|
-
|
|
15887
|
-
|
|
16610
|
+
creationPrompt: input.creationPrompt,
|
|
16611
|
+
followUpPrompt: input.followUpPrompt,
|
|
16612
|
+
launchMode: input.launchMode,
|
|
16613
|
+
source: input.source
|
|
15888
16614
|
}));
|
|
15889
16615
|
}
|
|
15890
16616
|
buildSessionLayout(input) {
|
|
15891
|
-
const
|
|
16617
|
+
const baseSystemPrompt = input.launchMode === "fresh" && input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
|
|
16618
|
+
const oneshotPrompt = input.launchMode === "fresh" && input.source === "oneshot" ? this.deps.config.oneshot.systemPrompt : undefined;
|
|
16619
|
+
const systemPrompt = baseSystemPrompt && oneshotPrompt ? `${baseSystemPrompt}
|
|
16620
|
+
|
|
16621
|
+
${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
|
|
16622
|
+
const prompt = input.launchMode === "resume" ? input.followUpPrompt : input.creationPrompt;
|
|
15892
16623
|
const containerName = input.containerName;
|
|
15893
16624
|
return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
|
|
15894
16625
|
repoRoot: this.deps.projectRoot,
|
|
@@ -15903,7 +16634,7 @@ class LifecycleService {
|
|
|
15903
16634
|
profileName: input.profileName,
|
|
15904
16635
|
yolo: input.profile.yolo === true,
|
|
15905
16636
|
systemPrompt,
|
|
15906
|
-
prompt
|
|
16637
|
+
prompt,
|
|
15907
16638
|
launchMode: input.launchMode
|
|
15908
16639
|
}),
|
|
15909
16640
|
shell: buildDockerShellCommand(containerName, input.worktreePath, input.initialized.paths.runtimeEnvPath)
|
|
@@ -15917,7 +16648,7 @@ class LifecycleService {
|
|
|
15917
16648
|
profileName: input.profileName,
|
|
15918
16649
|
yolo: input.profile.yolo === true,
|
|
15919
16650
|
systemPrompt,
|
|
15920
|
-
prompt
|
|
16651
|
+
prompt,
|
|
15921
16652
|
launchMode: input.launchMode
|
|
15922
16653
|
}),
|
|
15923
16654
|
shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
|
|
@@ -16041,12 +16772,14 @@ class LifecycleService {
|
|
|
16041
16772
|
const { profileName, profile } = this.resolveProfile(input.profile);
|
|
16042
16773
|
const agent = this.resolveAgentDefinition(input.agent);
|
|
16043
16774
|
const worktreePath = this.resolveWorktreePath(input.branch);
|
|
16775
|
+
const source = input.source ?? "ui";
|
|
16044
16776
|
const createProgressBase = {
|
|
16045
16777
|
branch: input.branch,
|
|
16046
16778
|
...baseBranch ? { baseBranch } : {},
|
|
16047
16779
|
path: worktreePath,
|
|
16048
16780
|
profile: profileName,
|
|
16049
|
-
agent: input.agent
|
|
16781
|
+
agent: input.agent,
|
|
16782
|
+
source
|
|
16050
16783
|
};
|
|
16051
16784
|
const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
|
|
16052
16785
|
let initialized = null;
|
|
@@ -16071,7 +16804,9 @@ class LifecycleService {
|
|
|
16071
16804
|
runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
|
|
16072
16805
|
controlUrl: this.controlUrl(profile.runtime),
|
|
16073
16806
|
controlToken: await this.deps.getControlToken(),
|
|
16074
|
-
deleteBranchOnRollback
|
|
16807
|
+
deleteBranchOnRollback,
|
|
16808
|
+
source,
|
|
16809
|
+
...input.oneshot ? { oneshot: input.oneshot } : {}
|
|
16075
16810
|
}, {
|
|
16076
16811
|
git: this.deps.git
|
|
16077
16812
|
});
|
|
@@ -16109,8 +16844,9 @@ class LifecycleService {
|
|
|
16109
16844
|
agent,
|
|
16110
16845
|
initialized,
|
|
16111
16846
|
worktreePath,
|
|
16112
|
-
|
|
16113
|
-
launchMode: "fresh"
|
|
16847
|
+
creationPrompt: input.prompt,
|
|
16848
|
+
launchMode: "fresh",
|
|
16849
|
+
source
|
|
16114
16850
|
});
|
|
16115
16851
|
await this.reportCreateProgress({
|
|
16116
16852
|
...createProgressBase,
|
|
@@ -16596,17 +17332,27 @@ function startPrMonitor(getWorktreeGitDirs, linkedRepos, projectDir, intervalMs
|
|
|
16596
17332
|
var LINEAR_AUTO_CREATE_POLL_INTERVAL_MS = 60000;
|
|
16597
17333
|
var processedIssueIds = new Set;
|
|
16598
17334
|
var AUTO_CREATE_LABEL = "webmux";
|
|
16599
|
-
|
|
17335
|
+
var AUTO_ONESHOT_LABEL = "webmux_oneshot";
|
|
17336
|
+
function hasLabel(issue, name) {
|
|
17337
|
+
return issue.labels.some((l) => l.name.toLowerCase() === name);
|
|
17338
|
+
}
|
|
17339
|
+
function filterTriggerableIssues(issues, existingBranches, matchesLabelRule) {
|
|
16600
17340
|
return issues.filter((issue) => {
|
|
16601
17341
|
if (issue.state.name !== "Todo")
|
|
16602
17342
|
return false;
|
|
16603
|
-
if (!issue
|
|
17343
|
+
if (!matchesLabelRule(issue))
|
|
16604
17344
|
return false;
|
|
16605
17345
|
if (processedIssueIds.has(issue.id))
|
|
16606
17346
|
return false;
|
|
16607
17347
|
return !existingBranches.some((branch) => branchMatchesIssue(branch, issue.branchName));
|
|
16608
17348
|
});
|
|
16609
17349
|
}
|
|
17350
|
+
function filterAutoCreateIssues(issues, existingBranches) {
|
|
17351
|
+
return filterTriggerableIssues(issues, existingBranches, (issue) => hasLabel(issue, AUTO_CREATE_LABEL) && !hasLabel(issue, AUTO_ONESHOT_LABEL));
|
|
17352
|
+
}
|
|
17353
|
+
function filterAutoOneshotIssues(issues, existingBranches) {
|
|
17354
|
+
return filterTriggerableIssues(issues, existingBranches, (issue) => hasLabel(issue, AUTO_ONESHOT_LABEL));
|
|
17355
|
+
}
|
|
16610
17356
|
async function runLinearAutoCreateOnce(deps) {
|
|
16611
17357
|
const fetchIssues = deps.fetchIssues ?? fetchAssignedIssues;
|
|
16612
17358
|
const result = await fetchIssues({ skipCache: true });
|
|
@@ -16614,29 +17360,53 @@ async function runLinearAutoCreateOnce(deps) {
|
|
|
16614
17360
|
log.error(`[linear-auto-create] failed to fetch issues: ${result.error}`);
|
|
16615
17361
|
return;
|
|
16616
17362
|
}
|
|
17363
|
+
const eligibleIssueIds = new Set(result.data.filter((issue) => issue.state.name === "Todo" && (hasLabel(issue, AUTO_CREATE_LABEL) || hasLabel(issue, AUTO_ONESHOT_LABEL))).map((issue) => issue.id));
|
|
17364
|
+
for (const id of processedIssueIds) {
|
|
17365
|
+
if (!eligibleIssueIds.has(id))
|
|
17366
|
+
processedIssueIds.delete(id);
|
|
17367
|
+
}
|
|
16617
17368
|
const projectRoot2 = deps.projectRoot;
|
|
16618
17369
|
const existingBranches = deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch);
|
|
16619
|
-
const
|
|
16620
|
-
|
|
17370
|
+
const oneshotIssues = deps.runOneshotForIssue ? filterAutoOneshotIssues(result.data, existingBranches) : [];
|
|
17371
|
+
const createIssues = filterAutoCreateIssues(result.data, existingBranches);
|
|
17372
|
+
if (oneshotIssues.length === 0 && createIssues.length === 0) {
|
|
16621
17373
|
log.debug(`[linear-auto-create] no new labeled issues (${result.data.length} assigned, ${existingBranches.length} worktrees)`);
|
|
16622
17374
|
return;
|
|
16623
17375
|
}
|
|
16624
|
-
|
|
16625
|
-
|
|
16626
|
-
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
|
|
17376
|
+
if (oneshotIssues.length > 0) {
|
|
17377
|
+
log.info(`[linear-auto-create] found ${oneshotIssues.length} new issue(s) with "${AUTO_ONESHOT_LABEL}" label`);
|
|
17378
|
+
for (const issue of oneshotIssues) {
|
|
17379
|
+
try {
|
|
17380
|
+
log.info(`[linear-auto-create] launching oneshot for ${issue.identifier}: ${issue.title}`);
|
|
17381
|
+
await deps.runOneshotForIssue(issue.identifier);
|
|
17382
|
+
processedIssueIds.add(issue.id);
|
|
17383
|
+
log.info(`[linear-auto-create] launched oneshot for ${issue.identifier}`);
|
|
17384
|
+
} catch (err) {
|
|
17385
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17386
|
+
log.error(`[linear-auto-create] failed to launch oneshot for ${issue.identifier}: ${msg}`);
|
|
17387
|
+
processedIssueIds.add(issue.id);
|
|
17388
|
+
}
|
|
17389
|
+
}
|
|
17390
|
+
}
|
|
17391
|
+
if (createIssues.length > 0) {
|
|
17392
|
+
log.info(`[linear-auto-create] found ${createIssues.length} new issue(s) with "${AUTO_CREATE_LABEL}" label`);
|
|
17393
|
+
for (const issue of createIssues) {
|
|
17394
|
+
try {
|
|
17395
|
+
log.info(`[linear-auto-create] creating worktree for ${issue.identifier}: ${issue.title}`);
|
|
17396
|
+
await deps.lifecycleService.createWorktree({
|
|
17397
|
+
mode: "new",
|
|
17398
|
+
branch: issue.branchName,
|
|
17399
|
+
prompt: `${issue.title}
|
|
16632
17400
|
|
|
16633
17401
|
${issue.description ?? ""}`.trim()
|
|
16634
|
-
|
|
16635
|
-
|
|
16636
|
-
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
|
|
17402
|
+
});
|
|
17403
|
+
processedIssueIds.add(issue.id);
|
|
17404
|
+
log.info(`[linear-auto-create] created worktree for ${issue.identifier}`);
|
|
17405
|
+
} catch (err) {
|
|
17406
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17407
|
+
log.error(`[linear-auto-create] failed to create worktree for ${issue.identifier}: ${msg}`);
|
|
17408
|
+
processedIssueIds.add(issue.id);
|
|
17409
|
+
}
|
|
16640
17410
|
}
|
|
16641
17411
|
}
|
|
16642
17412
|
}
|
|
@@ -16648,6 +17418,90 @@ function resetProcessedIssues() {
|
|
|
16648
17418
|
processedIssueIds.clear();
|
|
16649
17419
|
}
|
|
16650
17420
|
|
|
17421
|
+
// backend/src/services/oneshot-watcher-service.ts
|
|
17422
|
+
var POLL_INTERVAL_MS = 3000;
|
|
17423
|
+
var IDLE_GRACE_MS = 15000;
|
|
17424
|
+
var states = new Map;
|
|
17425
|
+
function getState(branch) {
|
|
17426
|
+
let state = states.get(branch);
|
|
17427
|
+
if (!state) {
|
|
17428
|
+
state = { idleSinceMs: null, inFlight: false };
|
|
17429
|
+
states.set(branch, state);
|
|
17430
|
+
}
|
|
17431
|
+
return state;
|
|
17432
|
+
}
|
|
17433
|
+
async function processWorktree(branch, path, agentLifecycle, hasPr, deps) {
|
|
17434
|
+
const readMeta = deps.readWorktreeMeta ?? readWorktreeMeta;
|
|
17435
|
+
const idleGrace = deps.idleGraceMs ?? IDLE_GRACE_MS;
|
|
17436
|
+
const now = deps.now ?? (() => Date.now());
|
|
17437
|
+
const meta = await readMeta(path);
|
|
17438
|
+
if (!meta?.oneshot) {
|
|
17439
|
+
states.delete(branch);
|
|
17440
|
+
return;
|
|
17441
|
+
}
|
|
17442
|
+
const state = getState(branch);
|
|
17443
|
+
if (state.inFlight)
|
|
17444
|
+
return;
|
|
17445
|
+
const isTerminal = agentLifecycle === "stopped" || agentLifecycle === "error";
|
|
17446
|
+
const needsGrace = agentLifecycle === "idle" || agentLifecycle === "closed";
|
|
17447
|
+
if (!isTerminal && !needsGrace) {
|
|
17448
|
+
state.idleSinceMs = null;
|
|
17449
|
+
return;
|
|
17450
|
+
}
|
|
17451
|
+
if (state.idleSinceMs === null)
|
|
17452
|
+
state.idleSinceMs = now();
|
|
17453
|
+
const stable = isTerminal || now() - state.idleSinceMs >= idleGrace;
|
|
17454
|
+
if (!stable)
|
|
17455
|
+
return;
|
|
17456
|
+
state.inFlight = true;
|
|
17457
|
+
try {
|
|
17458
|
+
const reason = isTerminal ? `agent ${agentLifecycle}` : agentLifecycle === "closed" ? "agent closed without resuming" : hasPr ? "agent idle after opening PR" : "agent idle without opening a PR";
|
|
17459
|
+
log.info(`[oneshot-watcher] ${branch}: ${reason} \u2014 firing end-of-run actions`);
|
|
17460
|
+
if (meta.oneshot.postToLinearOnDone) {
|
|
17461
|
+
try {
|
|
17462
|
+
await deps.postToLinear(branch, meta.oneshot.postToLinearOnDone);
|
|
17463
|
+
log.info(`[oneshot-watcher] ${branch}: posted conversation to Linear`);
|
|
17464
|
+
} catch (error) {
|
|
17465
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17466
|
+
log.error(`[oneshot-watcher] ${branch}: post-to-Linear failed \u2014 ${msg}`);
|
|
17467
|
+
}
|
|
17468
|
+
}
|
|
17469
|
+
if (meta.oneshot.autoCloseOnDone) {
|
|
17470
|
+
const fresh = await readMeta(path);
|
|
17471
|
+
if (!fresh?.oneshot) {
|
|
17472
|
+
log.info(`[oneshot-watcher] ${branch}: disarmed during post-to-Linear \u2014 skipping close`);
|
|
17473
|
+
return;
|
|
17474
|
+
}
|
|
17475
|
+
try {
|
|
17476
|
+
await deps.lifecycleService.closeWorktree(branch);
|
|
17477
|
+
log.info(`[oneshot-watcher] ${branch}: closed session`);
|
|
17478
|
+
} catch (error) {
|
|
17479
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17480
|
+
log.error(`[oneshot-watcher] ${branch}: close failed \u2014 ${msg}`);
|
|
17481
|
+
}
|
|
17482
|
+
}
|
|
17483
|
+
await deps.lifecycleService.disarmOneshot(branch);
|
|
17484
|
+
const runtimeState = deps.projectRuntime.getWorktreeByBranch(branch);
|
|
17485
|
+
if (runtimeState)
|
|
17486
|
+
deps.projectRuntime.setOneshot(runtimeState.worktreeId, null);
|
|
17487
|
+
} finally {
|
|
17488
|
+
states.delete(branch);
|
|
17489
|
+
}
|
|
17490
|
+
}
|
|
17491
|
+
async function runOneshotWatch(deps) {
|
|
17492
|
+
const worktrees = deps.projectRuntime.listWorktrees();
|
|
17493
|
+
for (const wt of worktrees) {
|
|
17494
|
+
if (wt.source !== "oneshot")
|
|
17495
|
+
continue;
|
|
17496
|
+
const hasPr = wt.prs.length > 0;
|
|
17497
|
+
await processWorktree(wt.branch, wt.path, wt.agent.lifecycle, hasPr, deps);
|
|
17498
|
+
}
|
|
17499
|
+
}
|
|
17500
|
+
function startOneshotWatcher(deps) {
|
|
17501
|
+
log.info("[oneshot-watcher] monitor started");
|
|
17502
|
+
return startSerializedInterval(() => runOneshotWatch(deps), deps.pollIntervalMs ?? POLL_INTERVAL_MS);
|
|
17503
|
+
}
|
|
17504
|
+
|
|
16651
17505
|
// backend/src/services/auto-remove-service.ts
|
|
16652
17506
|
async function runAutoRemove(deps) {
|
|
16653
17507
|
const worktrees = deps.git.listLiveWorktrees(deps.projectRoot).filter((e) => !e.bare && e.branch !== null && e.path !== deps.projectRoot);
|
|
@@ -16860,7 +17714,9 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue,
|
|
|
16860
17714
|
services: state.services.map((service) => ({ ...service })),
|
|
16861
17715
|
prs: state.prs.map((pr) => clonePrEntry(pr)),
|
|
16862
17716
|
linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
|
|
16863
|
-
creation: mapCreationSnapshot(creating)
|
|
17717
|
+
creation: mapCreationSnapshot(creating),
|
|
17718
|
+
source: state.source,
|
|
17719
|
+
oneshot: state.oneshot
|
|
16864
17720
|
};
|
|
16865
17721
|
}
|
|
16866
17722
|
function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
@@ -16883,7 +17739,9 @@ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, find
|
|
|
16883
17739
|
services: [],
|
|
16884
17740
|
prs: [],
|
|
16885
17741
|
linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
|
|
16886
|
-
creation: mapCreationSnapshot(creating)
|
|
17742
|
+
creation: mapCreationSnapshot(creating),
|
|
17743
|
+
source: creating.source,
|
|
17744
|
+
oneshot: null
|
|
16887
17745
|
};
|
|
16888
17746
|
}
|
|
16889
17747
|
function buildWorktreeSnapshots(input) {
|
|
@@ -17041,13 +17899,18 @@ class ClaudeConversationService {
|
|
|
17041
17899
|
}
|
|
17042
17900
|
async resolveSession(meta, cwd) {
|
|
17043
17901
|
const savedSessionId = isClaudeConversationMeta(meta.conversation) ? meta.conversation.sessionId : null;
|
|
17902
|
+
const discovered = (await this.deps.claude.listSessions(cwd))[0] ?? null;
|
|
17903
|
+
if (discovered && discovered.sessionId !== savedSessionId) {
|
|
17904
|
+
const session = await this.deps.claude.readSession(discovered.sessionId, cwd);
|
|
17905
|
+
if (session)
|
|
17906
|
+
return session;
|
|
17907
|
+
}
|
|
17044
17908
|
if (savedSessionId) {
|
|
17045
17909
|
const savedSession = await this.deps.claude.readSession(savedSessionId, cwd);
|
|
17046
17910
|
if (savedSession)
|
|
17047
17911
|
return savedSession;
|
|
17048
17912
|
log.warn(`[agents] saved Claude session missing, rediscovering cwd=${cwd} sessionId=${savedSessionId}`);
|
|
17049
17913
|
}
|
|
17050
|
-
const discovered = (await this.deps.claude.listSessions(cwd))[0] ?? null;
|
|
17051
17914
|
if (!discovered)
|
|
17052
17915
|
return null;
|
|
17053
17916
|
return await this.deps.claude.readSession(discovered.sessionId, cwd);
|
|
@@ -17561,7 +18424,7 @@ async function removeContainer(branch) {
|
|
|
17561
18424
|
}
|
|
17562
18425
|
|
|
17563
18426
|
// backend/src/adapters/hooks.ts
|
|
17564
|
-
import { join as
|
|
18427
|
+
import { join as join6 } from "path";
|
|
17565
18428
|
function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
17566
18429
|
const output = stderr.trim() || stdout.trim();
|
|
17567
18430
|
if (output) {
|
|
@@ -17586,7 +18449,7 @@ class BunLifecycleHookRunner {
|
|
|
17586
18449
|
return this.direnvAvailable;
|
|
17587
18450
|
}
|
|
17588
18451
|
async buildCommand(cwd, command) {
|
|
17589
|
-
if (this.checkDirenv() && await Bun.file(
|
|
18452
|
+
if (this.checkDirenv() && await Bun.file(join6(cwd, ".envrc")).exists()) {
|
|
17590
18453
|
Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
17591
18454
|
return ["direnv", "exec", cwd, "bash", "-c", command];
|
|
17592
18455
|
}
|
|
@@ -18006,6 +18869,8 @@ function makeDefaultState(input) {
|
|
|
18006
18869
|
path: input.path,
|
|
18007
18870
|
profile: input.profile ?? null,
|
|
18008
18871
|
agentName: input.agentName ?? null,
|
|
18872
|
+
source: input.source ?? "ui",
|
|
18873
|
+
oneshot: input.oneshot ?? null,
|
|
18009
18874
|
git: {
|
|
18010
18875
|
exists: true,
|
|
18011
18876
|
branch: input.branch,
|
|
@@ -18055,6 +18920,10 @@ class ProjectRuntime {
|
|
|
18055
18920
|
existing.agentName = input.agentName ?? existing.agentName;
|
|
18056
18921
|
if (input.runtime)
|
|
18057
18922
|
existing.agent.runtime = input.runtime;
|
|
18923
|
+
if (input.source !== undefined)
|
|
18924
|
+
existing.source = input.source;
|
|
18925
|
+
if (input.oneshot !== undefined)
|
|
18926
|
+
existing.oneshot = input.oneshot;
|
|
18058
18927
|
existing.git.exists = true;
|
|
18059
18928
|
existing.git.branch = input.branch;
|
|
18060
18929
|
existing.session.windowName = buildWorktreeWindowName(input.branch);
|
|
@@ -18065,6 +18934,11 @@ class ProjectRuntime {
|
|
|
18065
18934
|
this.worktreeIdsByBranch.set(input.branch, input.worktreeId);
|
|
18066
18935
|
return created;
|
|
18067
18936
|
}
|
|
18937
|
+
setOneshot(worktreeId, oneshot) {
|
|
18938
|
+
const state = this.requireWorktree(worktreeId);
|
|
18939
|
+
state.oneshot = oneshot;
|
|
18940
|
+
return state;
|
|
18941
|
+
}
|
|
18068
18942
|
removeWorktree(worktreeId) {
|
|
18069
18943
|
const state = this.worktrees.get(worktreeId);
|
|
18070
18944
|
if (!state)
|
|
@@ -18258,6 +19132,8 @@ class ReconciliationService {
|
|
|
18258
19132
|
profile: meta?.profile ?? null,
|
|
18259
19133
|
agentName: meta?.agent ?? null,
|
|
18260
19134
|
runtime: meta?.runtime ?? "host",
|
|
19135
|
+
source: meta?.source ?? "ui",
|
|
19136
|
+
oneshot: meta?.oneshot ?? null,
|
|
18261
19137
|
git: {
|
|
18262
19138
|
dirty: gitStatus.dirty,
|
|
18263
19139
|
aheadCount: gitStatus.aheadCount,
|
|
@@ -18290,7 +19166,9 @@ class ReconciliationService {
|
|
|
18290
19166
|
path: state.path,
|
|
18291
19167
|
profile: state.profile,
|
|
18292
19168
|
agentName: state.agentName,
|
|
18293
|
-
runtime: state.runtime
|
|
19169
|
+
runtime: state.runtime,
|
|
19170
|
+
source: state.source,
|
|
19171
|
+
oneshot: state.oneshot
|
|
18294
19172
|
});
|
|
18295
19173
|
this.deps.runtime.setGitState(state.worktreeId, {
|
|
18296
19174
|
exists: true,
|
|
@@ -18325,7 +19203,8 @@ class WorktreeCreationTracker {
|
|
|
18325
19203
|
path: progress.path,
|
|
18326
19204
|
profile: progress.profile,
|
|
18327
19205
|
agentName: progress.agent,
|
|
18328
|
-
phase: progress.phase
|
|
19206
|
+
phase: progress.phase,
|
|
19207
|
+
source: progress.source
|
|
18329
19208
|
};
|
|
18330
19209
|
this.worktrees.set(progress.branch, next);
|
|
18331
19210
|
}
|
|
@@ -18435,15 +19314,60 @@ var lifecycleService = runtime.lifecycleService;
|
|
|
18435
19314
|
var linearAutoCreateEnabled = config.integrations.linear.autoCreateWorktrees;
|
|
18436
19315
|
var stopLinearAutoCreate = null;
|
|
18437
19316
|
var autoRemoveOnMergeEnabled = config.integrations.github.autoRemoveOnMerge;
|
|
19317
|
+
async function runOneshotForIssue(issueId) {
|
|
19318
|
+
const seed = await buildSeedFromLinear({ issueId }, defaultSeedFromLinearDeps);
|
|
19319
|
+
if (!seed.ok) {
|
|
19320
|
+
throw new Error(`Linear seed failed for ${issueId}: ${seed.error}`);
|
|
19321
|
+
}
|
|
19322
|
+
const branch = seed.data.branch;
|
|
19323
|
+
if (!branch) {
|
|
19324
|
+
throw new Error(`Linear seed for ${issueId} did not resolve to a branch`);
|
|
19325
|
+
}
|
|
19326
|
+
const mode = seed.data.source !== "none" ? "existing" : "new";
|
|
19327
|
+
const prompt = seed.data.conversationMarkdown?.trim() ?? "";
|
|
19328
|
+
await lifecycleService.createWorktree({
|
|
19329
|
+
mode,
|
|
19330
|
+
branch,
|
|
19331
|
+
...prompt ? { prompt } : {},
|
|
19332
|
+
source: "oneshot",
|
|
19333
|
+
oneshot: {
|
|
19334
|
+
autoCloseOnDone: true,
|
|
19335
|
+
postToLinearOnDone: { kind: "issue", issueId }
|
|
19336
|
+
}
|
|
19337
|
+
});
|
|
19338
|
+
}
|
|
18438
19339
|
function startLinearAutoCreate() {
|
|
18439
19340
|
if (stopLinearAutoCreate)
|
|
18440
19341
|
return;
|
|
18441
19342
|
stopLinearAutoCreate = startLinearAutoCreateMonitor({
|
|
18442
19343
|
lifecycleService,
|
|
18443
19344
|
git,
|
|
18444
|
-
projectRoot: PROJECT_DIR
|
|
19345
|
+
projectRoot: PROJECT_DIR,
|
|
19346
|
+
runOneshotForIssue
|
|
18445
19347
|
});
|
|
18446
19348
|
}
|
|
19349
|
+
function normalizeOneshotConfig(input) {
|
|
19350
|
+
if (!input)
|
|
19351
|
+
return;
|
|
19352
|
+
return {
|
|
19353
|
+
autoCloseOnDone: input.autoCloseOnDone ?? true,
|
|
19354
|
+
...input.postToLinearOnDone ? { postToLinearOnDone: input.postToLinearOnDone } : {}
|
|
19355
|
+
};
|
|
19356
|
+
}
|
|
19357
|
+
async function disarmOneshotIfArmed(branch, reason) {
|
|
19358
|
+
try {
|
|
19359
|
+
const disarmed = await lifecycleService.disarmOneshot(branch);
|
|
19360
|
+
if (!disarmed)
|
|
19361
|
+
return;
|
|
19362
|
+
log.info(`[oneshot-watcher] ${branch}: disarmed by ${reason}`);
|
|
19363
|
+
const state = projectRuntime.getWorktreeByBranch(branch);
|
|
19364
|
+
if (state)
|
|
19365
|
+
projectRuntime.setOneshot(state.worktreeId, null);
|
|
19366
|
+
} catch (error) {
|
|
19367
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
19368
|
+
log.warn(`[oneshot-watcher] disarm failed for ${branch} (${reason}): ${msg}`);
|
|
19369
|
+
}
|
|
19370
|
+
}
|
|
18447
19371
|
function stopLinearAutoCreateMonitor() {
|
|
18448
19372
|
if (stopLinearAutoCreate) {
|
|
18449
19373
|
stopLinearAutoCreate();
|
|
@@ -18753,6 +19677,7 @@ async function apiGetAgentsWorktreeHistory(branch) {
|
|
|
18753
19677
|
}
|
|
18754
19678
|
async function apiSendAgentsWorktreeMessage(branch, req) {
|
|
18755
19679
|
touchDashboardActivity();
|
|
19680
|
+
await disarmOneshotIfArmed(branch, "agents-send-message");
|
|
18756
19681
|
const parsed = await parseJsonBody(req, AgentsSendMessageRequestSchema);
|
|
18757
19682
|
if (!parsed.ok)
|
|
18758
19683
|
return parsed.response;
|
|
@@ -18785,6 +19710,7 @@ async function apiSendAgentsWorktreeMessage(branch, req) {
|
|
|
18785
19710
|
}
|
|
18786
19711
|
async function apiInterruptAgentsWorktree(branch) {
|
|
18787
19712
|
touchDashboardActivity();
|
|
19713
|
+
await disarmOneshotIfArmed(branch, "agents-interrupt");
|
|
18788
19714
|
const resolved = await resolveAgentsWorktree(branch);
|
|
18789
19715
|
if (!resolved.ok)
|
|
18790
19716
|
return resolved.response;
|
|
@@ -18970,6 +19896,37 @@ async function apiCreateWorktree(req) {
|
|
|
18970
19896
|
return errorResponse("Prompt is required when creating a Linear ticket", 400);
|
|
18971
19897
|
}
|
|
18972
19898
|
let resolvedBranch = branch;
|
|
19899
|
+
let resolvedPrompt = prompt;
|
|
19900
|
+
let resolvedMode = mode;
|
|
19901
|
+
if (body.fromLinear) {
|
|
19902
|
+
if (createLinearTicket) {
|
|
19903
|
+
return errorResponse("fromLinear cannot be combined with createLinearTicket", 400);
|
|
19904
|
+
}
|
|
19905
|
+
let conversationContext = body.fromLinear.conversationContext?.trim() ?? "";
|
|
19906
|
+
let seedBranch = null;
|
|
19907
|
+
if (!conversationContext || !resolvedBranch) {
|
|
19908
|
+
const seedResult = await buildSeedFromLinear({ issueId: body.fromLinear.issueId }, defaultSeedFromLinearDeps);
|
|
19909
|
+
if (!seedResult.ok) {
|
|
19910
|
+
return errorResponse(`Linear seed lookup failed: ${seedResult.error}`, seedResult.status);
|
|
19911
|
+
}
|
|
19912
|
+
if (!conversationContext && seedResult.data.conversationMarkdown) {
|
|
19913
|
+
conversationContext = seedResult.data.conversationMarkdown;
|
|
19914
|
+
}
|
|
19915
|
+
seedBranch = seedResult.data.branch;
|
|
19916
|
+
if (!resolvedBranch && seedBranch) {
|
|
19917
|
+
resolvedBranch = seedBranch;
|
|
19918
|
+
if (seedResult.data.source !== "none")
|
|
19919
|
+
resolvedMode = "existing";
|
|
19920
|
+
}
|
|
19921
|
+
}
|
|
19922
|
+
if (conversationContext) {
|
|
19923
|
+
resolvedPrompt = resolvedPrompt ? `${conversationContext}
|
|
19924
|
+
|
|
19925
|
+
---
|
|
19926
|
+
|
|
19927
|
+
${resolvedPrompt}` : conversationContext;
|
|
19928
|
+
}
|
|
19929
|
+
}
|
|
18973
19930
|
if (createLinearTicket) {
|
|
18974
19931
|
const title = deriveLinearIssueTitle(linearTitle, prompt);
|
|
18975
19932
|
if (!title) {
|
|
@@ -18981,7 +19938,7 @@ async function apiCreateWorktree(req) {
|
|
|
18981
19938
|
}
|
|
18982
19939
|
const linearResult = await createLinearIssue({
|
|
18983
19940
|
title,
|
|
18984
|
-
description:
|
|
19941
|
+
description: resolvedPrompt ?? "",
|
|
18985
19942
|
teamId
|
|
18986
19943
|
});
|
|
18987
19944
|
if (!linearResult.ok) {
|
|
@@ -18999,15 +19956,18 @@ async function apiCreateWorktree(req) {
|
|
|
18999
19956
|
return errorResponse("Base branch must differ from branch name", 400);
|
|
19000
19957
|
}
|
|
19001
19958
|
}
|
|
19002
|
-
|
|
19959
|
+
const oneshot = normalizeOneshotConfig(body.oneshot);
|
|
19960
|
+
log.info(`[worktree:add] mode=${mode ?? "new"}${resolvedBranch ? ` branch=${resolvedBranch}` : ""}${baseBranch ? ` base=${baseBranch}` : ""}${profile ? ` profile=${profile}` : ""} agents=${selectedAgents.join(",")}${createLinearTicket ? " linearTicket=true" : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}${oneshot ? " oneshot=armed" : ""}`);
|
|
19003
19961
|
const result = await lifecycleService.createWorktrees({
|
|
19004
|
-
mode,
|
|
19962
|
+
mode: resolvedMode,
|
|
19005
19963
|
branch: resolvedBranch,
|
|
19006
19964
|
baseBranch,
|
|
19007
|
-
prompt,
|
|
19965
|
+
prompt: resolvedPrompt,
|
|
19008
19966
|
profile,
|
|
19009
19967
|
...agents && agents.length > 0 ? { agents } : { agent },
|
|
19010
|
-
envOverrides
|
|
19968
|
+
envOverrides,
|
|
19969
|
+
...body.source ? { source: body.source } : {},
|
|
19970
|
+
...oneshot ? { oneshot } : {}
|
|
19011
19971
|
});
|
|
19012
19972
|
log.debug(`[worktree:add] done branches=${result.branches.join(",")}`);
|
|
19013
19973
|
return jsonResponse({
|
|
@@ -19023,16 +19983,22 @@ async function apiDeleteWorktree(name) {
|
|
|
19023
19983
|
return jsonResponse({ ok: true });
|
|
19024
19984
|
});
|
|
19025
19985
|
}
|
|
19026
|
-
async function apiOpenWorktree(name) {
|
|
19986
|
+
async function apiOpenWorktree(name, req) {
|
|
19027
19987
|
ensureBranchNotBusy(name);
|
|
19028
|
-
|
|
19029
|
-
|
|
19988
|
+
const parsed = await parseJsonBody(req, OpenWorktreeRequestSchema);
|
|
19989
|
+
if (!parsed.ok)
|
|
19990
|
+
return parsed.response;
|
|
19991
|
+
const prompt = parsed.data.prompt?.trim() ? parsed.data.prompt.trim() : undefined;
|
|
19992
|
+
const oneshot = normalizeOneshotConfig(parsed.data.oneshot);
|
|
19993
|
+
log.info(`[worktree:open] name=${name}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}${oneshot ? " oneshot=armed" : ""}`);
|
|
19994
|
+
const result = await lifecycleService.openWorktree(name, { prompt, ...oneshot ? { oneshot } : {} });
|
|
19030
19995
|
log.debug(`[worktree:open] done name=${name} worktreeId=${result.worktreeId}`);
|
|
19031
19996
|
return jsonResponse({ ok: true });
|
|
19032
19997
|
}
|
|
19033
19998
|
async function apiCloseWorktree(name) {
|
|
19034
19999
|
ensureBranchNotBusy(name);
|
|
19035
20000
|
log.info(`[worktree:close] name=${name}`);
|
|
20001
|
+
await disarmOneshotIfArmed(name, "close-worktree");
|
|
19036
20002
|
await lifecycleService.closeWorktree(name);
|
|
19037
20003
|
log.debug(`[worktree:close] done name=${name}`);
|
|
19038
20004
|
return jsonResponse({ ok: true });
|
|
@@ -19044,6 +20010,7 @@ async function apiSetWorktreeArchived(name, req) {
|
|
|
19044
20010
|
return parsed.response;
|
|
19045
20011
|
const body = parsed.data;
|
|
19046
20012
|
log.info(`[worktree:archive] name=${name} archived=${body.archived}`);
|
|
20013
|
+
await disarmOneshotIfArmed(name, "archive-worktree");
|
|
19047
20014
|
await lifecycleService.setWorktreeArchived(name, body.archived);
|
|
19048
20015
|
log.debug(`[worktree:archive] done name=${name} archived=${body.archived}`);
|
|
19049
20016
|
return jsonResponse({ ok: true, archived: body.archived });
|
|
@@ -19068,6 +20035,7 @@ async function apiSendPrompt(name, req) {
|
|
|
19068
20035
|
const text = body.text;
|
|
19069
20036
|
const preamble = body.preamble;
|
|
19070
20037
|
log.info(`[worktree:send] name=${name} text="${text.slice(0, 80)}"`);
|
|
20038
|
+
await disarmOneshotIfArmed(name, "send-prompt");
|
|
19071
20039
|
const terminalWorktree = await resolveTerminalWorktree(name);
|
|
19072
20040
|
const submitDelayMs = resolveWorktreeTerminalSubmitDelayMs(terminalWorktree.agentName);
|
|
19073
20041
|
const result = await sendPrompt(terminalWorktree.worktreeId, terminalWorktree.attachTarget, text, 0, preamble, submitDelayMs);
|
|
@@ -19078,6 +20046,7 @@ async function apiSendPrompt(name, req) {
|
|
|
19078
20046
|
async function apiMergeWorktree(name) {
|
|
19079
20047
|
ensureBranchNotBusy(name);
|
|
19080
20048
|
log.info(`[worktree:merge] name=${name}`);
|
|
20049
|
+
await disarmOneshotIfArmed(name, "merge-worktree");
|
|
19081
20050
|
await lifecycleService.mergeWorktree(name);
|
|
19082
20051
|
log.debug(`[worktree:merge] done name=${name}`);
|
|
19083
20052
|
return jsonResponse({ ok: true });
|
|
@@ -19201,6 +20170,74 @@ async function apiPullMain(req) {
|
|
|
19201
20170
|
log.info(`[pull-main] ${repo || "main"} ${force ? "force " : ""}pull: ${result.status}`);
|
|
19202
20171
|
return jsonResponse(result);
|
|
19203
20172
|
}
|
|
20173
|
+
async function postWorktreeConversationToLinear(branch, target) {
|
|
20174
|
+
if (!config.integrations.linear.enabled) {
|
|
20175
|
+
return { ok: false, error: "Linear integration is disabled", status: 400 };
|
|
20176
|
+
}
|
|
20177
|
+
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
20178
|
+
if (!apiKey?.trim()) {
|
|
20179
|
+
return { ok: false, error: "LINEAR_API_KEY not set", status: 503 };
|
|
20180
|
+
}
|
|
20181
|
+
await reconciliationService.reconcile(PROJECT_DIR);
|
|
20182
|
+
const state = projectRuntime.getWorktreeByBranch(branch);
|
|
20183
|
+
if (!state)
|
|
20184
|
+
return { ok: false, error: `Worktree not found: ${branch}`, status: 404 };
|
|
20185
|
+
const resolved = await resolveAgentsWorktree(branch);
|
|
20186
|
+
if (!resolved.ok) {
|
|
20187
|
+
return { ok: false, error: `Worktree not found: ${branch}`, status: 404 };
|
|
20188
|
+
}
|
|
20189
|
+
const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
|
|
20190
|
+
if (!chatSupport.ok)
|
|
20191
|
+
return { ok: false, error: chatSupport.error, status: chatSupport.status };
|
|
20192
|
+
const conversationResult = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
|
|
20193
|
+
if (!conversationResult.ok)
|
|
20194
|
+
return { ok: false, error: conversationResult.error, status: conversationResult.status };
|
|
20195
|
+
const prUrl = (state.prs ?? []).find((pr) => pr.state === "open" || pr.state === "merged")?.url ?? null;
|
|
20196
|
+
const exportInput = {
|
|
20197
|
+
target,
|
|
20198
|
+
branch,
|
|
20199
|
+
baseBranch: state.baseBranch ?? null,
|
|
20200
|
+
agent: resolved.worktree.agentName ?? null,
|
|
20201
|
+
prUrl,
|
|
20202
|
+
conversation: conversationResult.data.conversation,
|
|
20203
|
+
webmuxVersion: package_default.version
|
|
20204
|
+
};
|
|
20205
|
+
const deps = {
|
|
20206
|
+
fetchIssueWithAttachments,
|
|
20207
|
+
fetchTeamByKey,
|
|
20208
|
+
createLinearIssue,
|
|
20209
|
+
uploadAttachmentFile,
|
|
20210
|
+
attachToIssue,
|
|
20211
|
+
createIssueComment
|
|
20212
|
+
};
|
|
20213
|
+
const result = await exportConversationToLinear(exportInput, deps);
|
|
20214
|
+
if (!result.ok)
|
|
20215
|
+
return { ok: false, error: result.error, status: result.status };
|
|
20216
|
+
return { ok: true, data: result.data };
|
|
20217
|
+
}
|
|
20218
|
+
async function apiPostWorktreeToLinear(name, req) {
|
|
20219
|
+
const parsed = await parseJsonBody(req, PostWorktreeToLinearRequestSchema);
|
|
20220
|
+
if (!parsed.ok)
|
|
20221
|
+
return parsed.response;
|
|
20222
|
+
const outcome = await postWorktreeConversationToLinear(name, parsed.data.target);
|
|
20223
|
+
if (!outcome.ok)
|
|
20224
|
+
return errorResponse(outcome.error, outcome.status);
|
|
20225
|
+
return jsonResponse({
|
|
20226
|
+
ok: true,
|
|
20227
|
+
issueId: outcome.data.issueId,
|
|
20228
|
+
issueUrl: outcome.data.issueUrl,
|
|
20229
|
+
commentUrl: outcome.data.commentUrl,
|
|
20230
|
+
attachmentUrl: outcome.data.attachmentUrl
|
|
20231
|
+
});
|
|
20232
|
+
}
|
|
20233
|
+
async function apiSyncWorktreePrs(name) {
|
|
20234
|
+
await syncPrStatus(getWorktreeGitDirs, config.integrations.github.linkedRepos, PROJECT_DIR);
|
|
20235
|
+
const snapshot = await readProjectSnapshot();
|
|
20236
|
+
const worktree = snapshot.worktrees.find((w) => w.branch === name);
|
|
20237
|
+
if (!worktree)
|
|
20238
|
+
return errorResponse(`Worktree not found: ${name}`, 404);
|
|
20239
|
+
return jsonResponse(worktree);
|
|
20240
|
+
}
|
|
19204
20241
|
async function apiGetLinearIssues() {
|
|
19205
20242
|
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
19206
20243
|
const fetchResult = config.integrations.linear.enabled && apiKey?.trim() ? await fetchAssignedIssues() : undefined;
|
|
@@ -19258,6 +20295,7 @@ async function apiUploadFiles(name, req) {
|
|
|
19258
20295
|
const state = projectRuntime.getWorktreeByBranch(name);
|
|
19259
20296
|
if (!state)
|
|
19260
20297
|
return errorResponse(`Worktree not found: ${name}`, 404);
|
|
20298
|
+
await disarmOneshotIfArmed(name, "upload-files");
|
|
19261
20299
|
let formData;
|
|
19262
20300
|
try {
|
|
19263
20301
|
formData = await req.formData();
|
|
@@ -19280,7 +20318,7 @@ async function apiUploadFiles(name, req) {
|
|
|
19280
20318
|
return errorResponse(`File too large: ${entry.name} (max 10MB)`, 400);
|
|
19281
20319
|
}
|
|
19282
20320
|
const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
|
|
19283
|
-
const destPath =
|
|
20321
|
+
const destPath = join7(uploadDir, safeName);
|
|
19284
20322
|
if (!resolve9(destPath).startsWith(uploadDir + "/")) {
|
|
19285
20323
|
return errorResponse("Invalid filename", 400);
|
|
19286
20324
|
}
|
|
@@ -19444,7 +20482,7 @@ Bun.serve({
|
|
|
19444
20482
|
if (!parsed.ok)
|
|
19445
20483
|
return parsed.response;
|
|
19446
20484
|
const name = parsed.data;
|
|
19447
|
-
return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name));
|
|
20485
|
+
return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name, req));
|
|
19448
20486
|
}
|
|
19449
20487
|
},
|
|
19450
20488
|
"/api/worktrees/:name/terminal-launch": {
|
|
@@ -19474,6 +20512,24 @@ Bun.serve({
|
|
|
19474
20512
|
return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
|
|
19475
20513
|
}
|
|
19476
20514
|
},
|
|
20515
|
+
[apiPaths.postWorktreeToLinear]: {
|
|
20516
|
+
POST: (req) => {
|
|
20517
|
+
const parsed = parseWorktreeNameParam(req.params);
|
|
20518
|
+
if (!parsed.ok)
|
|
20519
|
+
return parsed.response;
|
|
20520
|
+
const name = parsed.data;
|
|
20521
|
+
return catching(`POST /api/worktrees/${name}/linear/post`, () => apiPostWorktreeToLinear(name, req));
|
|
20522
|
+
}
|
|
20523
|
+
},
|
|
20524
|
+
[apiPaths.syncWorktreePrs]: {
|
|
20525
|
+
POST: (req) => {
|
|
20526
|
+
const parsed = parseWorktreeNameParam(req.params);
|
|
20527
|
+
if (!parsed.ok)
|
|
20528
|
+
return parsed.response;
|
|
20529
|
+
const name = parsed.data;
|
|
20530
|
+
return catching(`POST /api/worktrees/${name}/sync-prs`, () => apiSyncWorktreePrs(name));
|
|
20531
|
+
}
|
|
20532
|
+
},
|
|
19477
20533
|
[apiPaths.setWorktreeLabel]: {
|
|
19478
20534
|
PUT: (req) => {
|
|
19479
20535
|
const parsed = parseWorktreeNameParam(req.params);
|
|
@@ -19558,7 +20614,7 @@ Bun.serve({
|
|
|
19558
20614
|
const url = new URL(req.url);
|
|
19559
20615
|
if (STATIC_DIR) {
|
|
19560
20616
|
const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
|
|
19561
|
-
const filePath =
|
|
20617
|
+
const filePath = join7(STATIC_DIR, rawPath);
|
|
19562
20618
|
const staticRoot = resolve9(STATIC_DIR);
|
|
19563
20619
|
if (!resolve9(filePath).startsWith(staticRoot + "/")) {
|
|
19564
20620
|
return new Response("Forbidden", { status: 403 });
|
|
@@ -19568,7 +20624,7 @@ Bun.serve({
|
|
|
19568
20624
|
const headers = rawPath.startsWith("/assets/") ? { "Cache-Control": "public, max-age=31536000, immutable" } : {};
|
|
19569
20625
|
return new Response(file, { headers });
|
|
19570
20626
|
}
|
|
19571
|
-
return new Response(Bun.file(
|
|
20627
|
+
return new Response(Bun.file(join7(STATIC_DIR, "index.html")), {
|
|
19572
20628
|
headers: { "Cache-Control": "no-cache" }
|
|
19573
20629
|
});
|
|
19574
20630
|
}
|
|
@@ -19604,6 +20660,9 @@ Bun.serve({
|
|
|
19604
20660
|
const attachId = getAttachedSessionId(data, ws);
|
|
19605
20661
|
if (!attachId)
|
|
19606
20662
|
return;
|
|
20663
|
+
if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
|
|
20664
|
+
disarmOneshotIfArmed(branch, "terminal-ws-input");
|
|
20665
|
+
}
|
|
19607
20666
|
write(attachId, msg.data);
|
|
19608
20667
|
break;
|
|
19609
20668
|
}
|
|
@@ -19611,6 +20670,9 @@ Bun.serve({
|
|
|
19611
20670
|
const attachId = getAttachedSessionId(data, ws);
|
|
19612
20671
|
if (!attachId)
|
|
19613
20672
|
return;
|
|
20673
|
+
if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
|
|
20674
|
+
disarmOneshotIfArmed(branch, "terminal-ws-send-keys");
|
|
20675
|
+
}
|
|
19614
20676
|
await sendKeys(attachId, msg.hexBytes);
|
|
19615
20677
|
break;
|
|
19616
20678
|
}
|
|
@@ -19691,6 +20753,15 @@ startPrMonitor(getWorktreeGitDirs, config.integrations.github.linkedRepos, PROJE
|
|
|
19691
20753
|
if (linearAutoCreateEnabled) {
|
|
19692
20754
|
startLinearAutoCreate();
|
|
19693
20755
|
}
|
|
20756
|
+
startOneshotWatcher({
|
|
20757
|
+
projectRuntime,
|
|
20758
|
+
lifecycleService,
|
|
20759
|
+
postToLinear: async (branch, target) => {
|
|
20760
|
+
const outcome = await postWorktreeConversationToLinear(branch, target);
|
|
20761
|
+
if (!outcome.ok)
|
|
20762
|
+
throw new Error(outcome.error);
|
|
20763
|
+
}
|
|
20764
|
+
});
|
|
19694
20765
|
if (config.workspace.autoPull.enabled) {
|
|
19695
20766
|
startAutoPullMonitor({ git, projectRoot: PROJECT_DIR, mainBranch: config.workspace.mainBranch }, config.workspace.autoPull.intervalSeconds * 1000);
|
|
19696
20767
|
}
|