webmux 0.31.1 → 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 +1221 -123
- package/bin/webmux.js +1816 -242
- 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
|
}
|
|
@@ -14975,12 +15673,26 @@ import { randomUUID } from "crypto";
|
|
|
14975
15673
|
// backend/src/adapters/git.ts
|
|
14976
15674
|
import { readdirSync, rmSync, statSync } from "fs";
|
|
14977
15675
|
import { resolve as resolve6, join as join5 } from "path";
|
|
15676
|
+
function spawnGit(args, cwd) {
|
|
15677
|
+
try {
|
|
15678
|
+
return {
|
|
15679
|
+
ok: true,
|
|
15680
|
+
result: Bun.spawnSync(["git", ...args], {
|
|
15681
|
+
cwd,
|
|
15682
|
+
stdout: "pipe",
|
|
15683
|
+
stderr: "pipe"
|
|
15684
|
+
})
|
|
15685
|
+
};
|
|
15686
|
+
} catch (error) {
|
|
15687
|
+
return { ok: false, stderr: `spawn error (cwd=${cwd}): ${errorMessage(error)}` };
|
|
15688
|
+
}
|
|
15689
|
+
}
|
|
14978
15690
|
function runGit(args, cwd) {
|
|
14979
|
-
const
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
}
|
|
15691
|
+
const spawned = spawnGit(args, cwd);
|
|
15692
|
+
if (!spawned.ok) {
|
|
15693
|
+
throw new Error(`git ${args.join(" ")} failed: ${spawned.stderr}`);
|
|
15694
|
+
}
|
|
15695
|
+
const { result } = spawned;
|
|
14984
15696
|
if (result.exitCode !== 0) {
|
|
14985
15697
|
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
14986
15698
|
throw new Error(`git ${args.join(" ")} failed: ${stderr || `exit ${result.exitCode}`}`);
|
|
@@ -14988,11 +15700,11 @@ function runGit(args, cwd) {
|
|
|
14988
15700
|
return new TextDecoder().decode(result.stdout).trim();
|
|
14989
15701
|
}
|
|
14990
15702
|
function tryRunGit(args, cwd) {
|
|
14991
|
-
const
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
}
|
|
15703
|
+
const spawned = spawnGit(args, cwd);
|
|
15704
|
+
if (!spawned.ok) {
|
|
15705
|
+
return { ok: false, stderr: spawned.stderr };
|
|
15706
|
+
}
|
|
15707
|
+
const { result } = spawned;
|
|
14996
15708
|
if (result.exitCode !== 0) {
|
|
14997
15709
|
return {
|
|
14998
15710
|
ok: false,
|
|
@@ -15113,6 +15825,16 @@ function listGitWorktrees(cwd) {
|
|
|
15113
15825
|
const output = runGit(["worktree", "list", "--porcelain"], cwd);
|
|
15114
15826
|
return parseGitWorktreePorcelain(output);
|
|
15115
15827
|
}
|
|
15828
|
+
function worktreeEntryPathExists(entry) {
|
|
15829
|
+
try {
|
|
15830
|
+
return statSync(entry.path).isDirectory();
|
|
15831
|
+
} catch {
|
|
15832
|
+
return false;
|
|
15833
|
+
}
|
|
15834
|
+
}
|
|
15835
|
+
function filterLiveWorktreeEntries(entries) {
|
|
15836
|
+
return entries.filter(worktreeEntryPathExists);
|
|
15837
|
+
}
|
|
15116
15838
|
function listLocalGitBranches(cwd) {
|
|
15117
15839
|
const output = runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd);
|
|
15118
15840
|
return output.split(`
|
|
@@ -15173,6 +15895,9 @@ class BunGitGateway {
|
|
|
15173
15895
|
listWorktrees(cwd) {
|
|
15174
15896
|
return listGitWorktrees(cwd);
|
|
15175
15897
|
}
|
|
15898
|
+
listLiveWorktrees(cwd) {
|
|
15899
|
+
return filterLiveWorktreeEntries(listGitWorktrees(cwd));
|
|
15900
|
+
}
|
|
15176
15901
|
listLocalBranches(cwd) {
|
|
15177
15902
|
return listLocalGitBranches(cwd);
|
|
15178
15903
|
}
|
|
@@ -15326,7 +16051,9 @@ async function initializeManagedWorktree(opts) {
|
|
|
15326
16051
|
agent: opts.agent,
|
|
15327
16052
|
runtime: opts.runtime,
|
|
15328
16053
|
startupEnvValues: { ...opts.startupEnvValues ?? {} },
|
|
15329
|
-
allocatedPorts: { ...opts.allocatedPorts ?? {} }
|
|
16054
|
+
allocatedPorts: { ...opts.allocatedPorts ?? {} },
|
|
16055
|
+
...opts.source ? { source: opts.source } : {},
|
|
16056
|
+
...opts.oneshot ? { oneshot: opts.oneshot } : {}
|
|
15330
16057
|
};
|
|
15331
16058
|
const paths = await ensureWorktreeStorageDirs(opts.gitDir);
|
|
15332
16059
|
await writeWorktreeMeta(opts.gitDir, meta);
|
|
@@ -15379,7 +16106,9 @@ async function createManagedWorktree(opts, deps = {}) {
|
|
|
15379
16106
|
controlUrl: opts.controlUrl,
|
|
15380
16107
|
controlToken: opts.controlToken,
|
|
15381
16108
|
now: opts.now,
|
|
15382
|
-
worktreeId: opts.worktreeId
|
|
16109
|
+
worktreeId: opts.worktreeId,
|
|
16110
|
+
...opts.source ? { source: opts.source } : {},
|
|
16111
|
+
...opts.oneshot ? { oneshot: opts.oneshot } : {}
|
|
15383
16112
|
});
|
|
15384
16113
|
if (deps.tmux) {
|
|
15385
16114
|
sessionLayoutPlan = sessionLayoutPlan ?? opts.sessionLayoutPlanBuilder?.(initialized);
|
|
@@ -15528,10 +16257,15 @@ class LifecycleService {
|
|
|
15528
16257
|
agent: agent.id
|
|
15529
16258
|
});
|
|
15530
16259
|
}
|
|
15531
|
-
async openWorktree(branch) {
|
|
16260
|
+
async openWorktree(branch, options = {}) {
|
|
15532
16261
|
try {
|
|
15533
16262
|
const resolved = await this.resolveExistingWorktree(branch);
|
|
15534
|
-
|
|
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
|
+
}
|
|
15535
16269
|
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
15536
16270
|
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
15537
16271
|
const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
|
|
@@ -15546,7 +16280,8 @@ class LifecycleService {
|
|
|
15546
16280
|
agent,
|
|
15547
16281
|
initialized,
|
|
15548
16282
|
worktreePath: resolved.entry.path,
|
|
15549
|
-
launchMode
|
|
16283
|
+
launchMode,
|
|
16284
|
+
followUpPrompt: options.prompt
|
|
15550
16285
|
});
|
|
15551
16286
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
15552
16287
|
return {
|
|
@@ -15557,6 +16292,20 @@ class LifecycleService {
|
|
|
15557
16292
|
throw this.wrapOperationError(error);
|
|
15558
16293
|
}
|
|
15559
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
|
+
}
|
|
15560
16309
|
async closeWorktree(branch) {
|
|
15561
16310
|
try {
|
|
15562
16311
|
await this.resolveExistingWorktree(branch);
|
|
@@ -15736,7 +16485,7 @@ class LifecycleService {
|
|
|
15736
16485
|
}
|
|
15737
16486
|
listProjectWorktrees() {
|
|
15738
16487
|
const projectRoot2 = resolve7(this.deps.projectRoot);
|
|
15739
|
-
return this.deps.git.
|
|
16488
|
+
return this.deps.git.listLiveWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve7(entry.path) !== projectRoot2);
|
|
15740
16489
|
}
|
|
15741
16490
|
async readManagedMetas() {
|
|
15742
16491
|
const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
|
|
@@ -15843,8 +16592,10 @@ class LifecycleService {
|
|
|
15843
16592
|
agent: input.agent,
|
|
15844
16593
|
initialized: input.initialized,
|
|
15845
16594
|
worktreePath: input.worktreePath,
|
|
15846
|
-
|
|
16595
|
+
creationPrompt: input.creationPrompt,
|
|
16596
|
+
followUpPrompt: input.followUpPrompt,
|
|
15847
16597
|
launchMode: input.launchMode,
|
|
16598
|
+
source: input.source,
|
|
15848
16599
|
containerName
|
|
15849
16600
|
}));
|
|
15850
16601
|
return;
|
|
@@ -15856,12 +16607,19 @@ class LifecycleService {
|
|
|
15856
16607
|
agent: input.agent,
|
|
15857
16608
|
initialized: input.initialized,
|
|
15858
16609
|
worktreePath: input.worktreePath,
|
|
15859
|
-
|
|
15860
|
-
|
|
16610
|
+
creationPrompt: input.creationPrompt,
|
|
16611
|
+
followUpPrompt: input.followUpPrompt,
|
|
16612
|
+
launchMode: input.launchMode,
|
|
16613
|
+
source: input.source
|
|
15861
16614
|
}));
|
|
15862
16615
|
}
|
|
15863
16616
|
buildSessionLayout(input) {
|
|
15864
|
-
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;
|
|
15865
16623
|
const containerName = input.containerName;
|
|
15866
16624
|
return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
|
|
15867
16625
|
repoRoot: this.deps.projectRoot,
|
|
@@ -15876,7 +16634,7 @@ class LifecycleService {
|
|
|
15876
16634
|
profileName: input.profileName,
|
|
15877
16635
|
yolo: input.profile.yolo === true,
|
|
15878
16636
|
systemPrompt,
|
|
15879
|
-
prompt
|
|
16637
|
+
prompt,
|
|
15880
16638
|
launchMode: input.launchMode
|
|
15881
16639
|
}),
|
|
15882
16640
|
shell: buildDockerShellCommand(containerName, input.worktreePath, input.initialized.paths.runtimeEnvPath)
|
|
@@ -15890,7 +16648,7 @@ class LifecycleService {
|
|
|
15890
16648
|
profileName: input.profileName,
|
|
15891
16649
|
yolo: input.profile.yolo === true,
|
|
15892
16650
|
systemPrompt,
|
|
15893
|
-
prompt
|
|
16651
|
+
prompt,
|
|
15894
16652
|
launchMode: input.launchMode
|
|
15895
16653
|
}),
|
|
15896
16654
|
shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
|
|
@@ -16014,12 +16772,14 @@ class LifecycleService {
|
|
|
16014
16772
|
const { profileName, profile } = this.resolveProfile(input.profile);
|
|
16015
16773
|
const agent = this.resolveAgentDefinition(input.agent);
|
|
16016
16774
|
const worktreePath = this.resolveWorktreePath(input.branch);
|
|
16775
|
+
const source = input.source ?? "ui";
|
|
16017
16776
|
const createProgressBase = {
|
|
16018
16777
|
branch: input.branch,
|
|
16019
16778
|
...baseBranch ? { baseBranch } : {},
|
|
16020
16779
|
path: worktreePath,
|
|
16021
16780
|
profile: profileName,
|
|
16022
|
-
agent: input.agent
|
|
16781
|
+
agent: input.agent,
|
|
16782
|
+
source
|
|
16023
16783
|
};
|
|
16024
16784
|
const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
|
|
16025
16785
|
let initialized = null;
|
|
@@ -16044,7 +16804,9 @@ class LifecycleService {
|
|
|
16044
16804
|
runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
|
|
16045
16805
|
controlUrl: this.controlUrl(profile.runtime),
|
|
16046
16806
|
controlToken: await this.deps.getControlToken(),
|
|
16047
|
-
deleteBranchOnRollback
|
|
16807
|
+
deleteBranchOnRollback,
|
|
16808
|
+
source,
|
|
16809
|
+
...input.oneshot ? { oneshot: input.oneshot } : {}
|
|
16048
16810
|
}, {
|
|
16049
16811
|
git: this.deps.git
|
|
16050
16812
|
});
|
|
@@ -16082,8 +16844,9 @@ class LifecycleService {
|
|
|
16082
16844
|
agent,
|
|
16083
16845
|
initialized,
|
|
16084
16846
|
worktreePath,
|
|
16085
|
-
|
|
16086
|
-
launchMode: "fresh"
|
|
16847
|
+
creationPrompt: input.prompt,
|
|
16848
|
+
launchMode: "fresh",
|
|
16849
|
+
source
|
|
16087
16850
|
});
|
|
16088
16851
|
await this.reportCreateProgress({
|
|
16089
16852
|
...createProgressBase,
|
|
@@ -16569,17 +17332,27 @@ function startPrMonitor(getWorktreeGitDirs, linkedRepos, projectDir, intervalMs
|
|
|
16569
17332
|
var LINEAR_AUTO_CREATE_POLL_INTERVAL_MS = 60000;
|
|
16570
17333
|
var processedIssueIds = new Set;
|
|
16571
17334
|
var AUTO_CREATE_LABEL = "webmux";
|
|
16572
|
-
|
|
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) {
|
|
16573
17340
|
return issues.filter((issue) => {
|
|
16574
17341
|
if (issue.state.name !== "Todo")
|
|
16575
17342
|
return false;
|
|
16576
|
-
if (!issue
|
|
17343
|
+
if (!matchesLabelRule(issue))
|
|
16577
17344
|
return false;
|
|
16578
17345
|
if (processedIssueIds.has(issue.id))
|
|
16579
17346
|
return false;
|
|
16580
17347
|
return !existingBranches.some((branch) => branchMatchesIssue(branch, issue.branchName));
|
|
16581
17348
|
});
|
|
16582
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
|
+
}
|
|
16583
17356
|
async function runLinearAutoCreateOnce(deps) {
|
|
16584
17357
|
const fetchIssues = deps.fetchIssues ?? fetchAssignedIssues;
|
|
16585
17358
|
const result = await fetchIssues({ skipCache: true });
|
|
@@ -16587,29 +17360,53 @@ async function runLinearAutoCreateOnce(deps) {
|
|
|
16587
17360
|
log.error(`[linear-auto-create] failed to fetch issues: ${result.error}`);
|
|
16588
17361
|
return;
|
|
16589
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
|
+
}
|
|
16590
17368
|
const projectRoot2 = deps.projectRoot;
|
|
16591
17369
|
const existingBranches = deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch);
|
|
16592
|
-
const
|
|
16593
|
-
|
|
17370
|
+
const oneshotIssues = deps.runOneshotForIssue ? filterAutoOneshotIssues(result.data, existingBranches) : [];
|
|
17371
|
+
const createIssues = filterAutoCreateIssues(result.data, existingBranches);
|
|
17372
|
+
if (oneshotIssues.length === 0 && createIssues.length === 0) {
|
|
16594
17373
|
log.debug(`[linear-auto-create] no new labeled issues (${result.data.length} assigned, ${existingBranches.length} worktrees)`);
|
|
16595
17374
|
return;
|
|
16596
17375
|
}
|
|
16597
|
-
|
|
16598
|
-
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
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}
|
|
16605
17400
|
|
|
16606
17401
|
${issue.description ?? ""}`.trim()
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
|
|
16610
|
-
|
|
16611
|
-
|
|
16612
|
-
|
|
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
|
+
}
|
|
16613
17410
|
}
|
|
16614
17411
|
}
|
|
16615
17412
|
}
|
|
@@ -16621,9 +17418,93 @@ function resetProcessedIssues() {
|
|
|
16621
17418
|
processedIssueIds.clear();
|
|
16622
17419
|
}
|
|
16623
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
|
+
|
|
16624
17505
|
// backend/src/services/auto-remove-service.ts
|
|
16625
17506
|
async function runAutoRemove(deps) {
|
|
16626
|
-
const worktrees = deps.git.
|
|
17507
|
+
const worktrees = deps.git.listLiveWorktrees(deps.projectRoot).filter((e) => !e.bare && e.branch !== null && e.path !== deps.projectRoot);
|
|
16627
17508
|
for (const entry of worktrees) {
|
|
16628
17509
|
const branch = entry.branch;
|
|
16629
17510
|
if (deps.isRemoving(branch))
|
|
@@ -16833,7 +17714,9 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue,
|
|
|
16833
17714
|
services: state.services.map((service) => ({ ...service })),
|
|
16834
17715
|
prs: state.prs.map((pr) => clonePrEntry(pr)),
|
|
16835
17716
|
linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
|
|
16836
|
-
creation: mapCreationSnapshot(creating)
|
|
17717
|
+
creation: mapCreationSnapshot(creating),
|
|
17718
|
+
source: state.source,
|
|
17719
|
+
oneshot: state.oneshot
|
|
16837
17720
|
};
|
|
16838
17721
|
}
|
|
16839
17722
|
function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
@@ -16856,7 +17739,9 @@ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, find
|
|
|
16856
17739
|
services: [],
|
|
16857
17740
|
prs: [],
|
|
16858
17741
|
linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
|
|
16859
|
-
creation: mapCreationSnapshot(creating)
|
|
17742
|
+
creation: mapCreationSnapshot(creating),
|
|
17743
|
+
source: creating.source,
|
|
17744
|
+
oneshot: null
|
|
16860
17745
|
};
|
|
16861
17746
|
}
|
|
16862
17747
|
function buildWorktreeSnapshots(input) {
|
|
@@ -17014,13 +17899,18 @@ class ClaudeConversationService {
|
|
|
17014
17899
|
}
|
|
17015
17900
|
async resolveSession(meta, cwd) {
|
|
17016
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
|
+
}
|
|
17017
17908
|
if (savedSessionId) {
|
|
17018
17909
|
const savedSession = await this.deps.claude.readSession(savedSessionId, cwd);
|
|
17019
17910
|
if (savedSession)
|
|
17020
17911
|
return savedSession;
|
|
17021
17912
|
log.warn(`[agents] saved Claude session missing, rediscovering cwd=${cwd} sessionId=${savedSessionId}`);
|
|
17022
17913
|
}
|
|
17023
|
-
const discovered = (await this.deps.claude.listSessions(cwd))[0] ?? null;
|
|
17024
17914
|
if (!discovered)
|
|
17025
17915
|
return null;
|
|
17026
17916
|
return await this.deps.claude.readSession(discovered.sessionId, cwd);
|
|
@@ -17534,7 +18424,7 @@ async function removeContainer(branch) {
|
|
|
17534
18424
|
}
|
|
17535
18425
|
|
|
17536
18426
|
// backend/src/adapters/hooks.ts
|
|
17537
|
-
import { join as
|
|
18427
|
+
import { join as join6 } from "path";
|
|
17538
18428
|
function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
17539
18429
|
const output = stderr.trim() || stdout.trim();
|
|
17540
18430
|
if (output) {
|
|
@@ -17559,7 +18449,7 @@ class BunLifecycleHookRunner {
|
|
|
17559
18449
|
return this.direnvAvailable;
|
|
17560
18450
|
}
|
|
17561
18451
|
async buildCommand(cwd, command) {
|
|
17562
|
-
if (this.checkDirenv() && await Bun.file(
|
|
18452
|
+
if (this.checkDirenv() && await Bun.file(join6(cwd, ".envrc")).exists()) {
|
|
17563
18453
|
Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
17564
18454
|
return ["direnv", "exec", cwd, "bash", "-c", command];
|
|
17565
18455
|
}
|
|
@@ -17979,6 +18869,8 @@ function makeDefaultState(input) {
|
|
|
17979
18869
|
path: input.path,
|
|
17980
18870
|
profile: input.profile ?? null,
|
|
17981
18871
|
agentName: input.agentName ?? null,
|
|
18872
|
+
source: input.source ?? "ui",
|
|
18873
|
+
oneshot: input.oneshot ?? null,
|
|
17982
18874
|
git: {
|
|
17983
18875
|
exists: true,
|
|
17984
18876
|
branch: input.branch,
|
|
@@ -18028,6 +18920,10 @@ class ProjectRuntime {
|
|
|
18028
18920
|
existing.agentName = input.agentName ?? existing.agentName;
|
|
18029
18921
|
if (input.runtime)
|
|
18030
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;
|
|
18031
18927
|
existing.git.exists = true;
|
|
18032
18928
|
existing.git.branch = input.branch;
|
|
18033
18929
|
existing.session.windowName = buildWorktreeWindowName(input.branch);
|
|
@@ -18038,6 +18934,11 @@ class ProjectRuntime {
|
|
|
18038
18934
|
this.worktreeIdsByBranch.set(input.branch, input.worktreeId);
|
|
18039
18935
|
return created;
|
|
18040
18936
|
}
|
|
18937
|
+
setOneshot(worktreeId, oneshot) {
|
|
18938
|
+
const state = this.requireWorktree(worktreeId);
|
|
18939
|
+
state.oneshot = oneshot;
|
|
18940
|
+
return state;
|
|
18941
|
+
}
|
|
18041
18942
|
removeWorktree(worktreeId) {
|
|
18042
18943
|
const state = this.worktrees.get(worktreeId);
|
|
18043
18944
|
if (!state)
|
|
@@ -18205,7 +19106,7 @@ class ReconciliationService {
|
|
|
18205
19106
|
return await this.inFlight;
|
|
18206
19107
|
}
|
|
18207
19108
|
async runReconcile(normalizedRepoRoot) {
|
|
18208
|
-
const worktrees = this.deps.git.
|
|
19109
|
+
const worktrees = this.deps.git.listLiveWorktrees(normalizedRepoRoot);
|
|
18209
19110
|
const sessionName = buildProjectSessionName(normalizedRepoRoot);
|
|
18210
19111
|
let windows = [];
|
|
18211
19112
|
try {
|
|
@@ -18231,6 +19132,8 @@ class ReconciliationService {
|
|
|
18231
19132
|
profile: meta?.profile ?? null,
|
|
18232
19133
|
agentName: meta?.agent ?? null,
|
|
18233
19134
|
runtime: meta?.runtime ?? "host",
|
|
19135
|
+
source: meta?.source ?? "ui",
|
|
19136
|
+
oneshot: meta?.oneshot ?? null,
|
|
18234
19137
|
git: {
|
|
18235
19138
|
dirty: gitStatus.dirty,
|
|
18236
19139
|
aheadCount: gitStatus.aheadCount,
|
|
@@ -18263,7 +19166,9 @@ class ReconciliationService {
|
|
|
18263
19166
|
path: state.path,
|
|
18264
19167
|
profile: state.profile,
|
|
18265
19168
|
agentName: state.agentName,
|
|
18266
|
-
runtime: state.runtime
|
|
19169
|
+
runtime: state.runtime,
|
|
19170
|
+
source: state.source,
|
|
19171
|
+
oneshot: state.oneshot
|
|
18267
19172
|
});
|
|
18268
19173
|
this.deps.runtime.setGitState(state.worktreeId, {
|
|
18269
19174
|
exists: true,
|
|
@@ -18298,7 +19203,8 @@ class WorktreeCreationTracker {
|
|
|
18298
19203
|
path: progress.path,
|
|
18299
19204
|
profile: progress.profile,
|
|
18300
19205
|
agentName: progress.agent,
|
|
18301
|
-
phase: progress.phase
|
|
19206
|
+
phase: progress.phase,
|
|
19207
|
+
source: progress.source
|
|
18302
19208
|
};
|
|
18303
19209
|
this.worktrees.set(progress.branch, next);
|
|
18304
19210
|
}
|
|
@@ -18408,15 +19314,60 @@ var lifecycleService = runtime.lifecycleService;
|
|
|
18408
19314
|
var linearAutoCreateEnabled = config.integrations.linear.autoCreateWorktrees;
|
|
18409
19315
|
var stopLinearAutoCreate = null;
|
|
18410
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
|
+
}
|
|
18411
19339
|
function startLinearAutoCreate() {
|
|
18412
19340
|
if (stopLinearAutoCreate)
|
|
18413
19341
|
return;
|
|
18414
19342
|
stopLinearAutoCreate = startLinearAutoCreateMonitor({
|
|
18415
19343
|
lifecycleService,
|
|
18416
19344
|
git,
|
|
18417
|
-
projectRoot: PROJECT_DIR
|
|
19345
|
+
projectRoot: PROJECT_DIR,
|
|
19346
|
+
runOneshotForIssue
|
|
18418
19347
|
});
|
|
18419
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
|
+
}
|
|
18420
19371
|
function stopLinearAutoCreateMonitor() {
|
|
18421
19372
|
if (stopLinearAutoCreate) {
|
|
18422
19373
|
stopLinearAutoCreate();
|
|
@@ -18610,7 +19561,7 @@ async function hasValidControlToken(req) {
|
|
|
18610
19561
|
async function getWorktreeGitDirs() {
|
|
18611
19562
|
const gitDirs = new Map;
|
|
18612
19563
|
const projectRoot2 = resolve9(PROJECT_DIR);
|
|
18613
|
-
for (const entry of git.
|
|
19564
|
+
for (const entry of git.listLiveWorktrees(projectRoot2)) {
|
|
18614
19565
|
if (entry.bare || resolve9(entry.path) === projectRoot2 || !entry.branch)
|
|
18615
19566
|
continue;
|
|
18616
19567
|
gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
|
|
@@ -18726,6 +19677,7 @@ async function apiGetAgentsWorktreeHistory(branch) {
|
|
|
18726
19677
|
}
|
|
18727
19678
|
async function apiSendAgentsWorktreeMessage(branch, req) {
|
|
18728
19679
|
touchDashboardActivity();
|
|
19680
|
+
await disarmOneshotIfArmed(branch, "agents-send-message");
|
|
18729
19681
|
const parsed = await parseJsonBody(req, AgentsSendMessageRequestSchema);
|
|
18730
19682
|
if (!parsed.ok)
|
|
18731
19683
|
return parsed.response;
|
|
@@ -18758,6 +19710,7 @@ async function apiSendAgentsWorktreeMessage(branch, req) {
|
|
|
18758
19710
|
}
|
|
18759
19711
|
async function apiInterruptAgentsWorktree(branch) {
|
|
18760
19712
|
touchDashboardActivity();
|
|
19713
|
+
await disarmOneshotIfArmed(branch, "agents-interrupt");
|
|
18761
19714
|
const resolved = await resolveAgentsWorktree(branch);
|
|
18762
19715
|
if (!resolved.ok)
|
|
18763
19716
|
return resolved.response;
|
|
@@ -18943,6 +19896,37 @@ async function apiCreateWorktree(req) {
|
|
|
18943
19896
|
return errorResponse("Prompt is required when creating a Linear ticket", 400);
|
|
18944
19897
|
}
|
|
18945
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
|
+
}
|
|
18946
19930
|
if (createLinearTicket) {
|
|
18947
19931
|
const title = deriveLinearIssueTitle(linearTitle, prompt);
|
|
18948
19932
|
if (!title) {
|
|
@@ -18954,7 +19938,7 @@ async function apiCreateWorktree(req) {
|
|
|
18954
19938
|
}
|
|
18955
19939
|
const linearResult = await createLinearIssue({
|
|
18956
19940
|
title,
|
|
18957
|
-
description:
|
|
19941
|
+
description: resolvedPrompt ?? "",
|
|
18958
19942
|
teamId
|
|
18959
19943
|
});
|
|
18960
19944
|
if (!linearResult.ok) {
|
|
@@ -18972,15 +19956,18 @@ async function apiCreateWorktree(req) {
|
|
|
18972
19956
|
return errorResponse("Base branch must differ from branch name", 400);
|
|
18973
19957
|
}
|
|
18974
19958
|
}
|
|
18975
|
-
|
|
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" : ""}`);
|
|
18976
19961
|
const result = await lifecycleService.createWorktrees({
|
|
18977
|
-
mode,
|
|
19962
|
+
mode: resolvedMode,
|
|
18978
19963
|
branch: resolvedBranch,
|
|
18979
19964
|
baseBranch,
|
|
18980
|
-
prompt,
|
|
19965
|
+
prompt: resolvedPrompt,
|
|
18981
19966
|
profile,
|
|
18982
19967
|
...agents && agents.length > 0 ? { agents } : { agent },
|
|
18983
|
-
envOverrides
|
|
19968
|
+
envOverrides,
|
|
19969
|
+
...body.source ? { source: body.source } : {},
|
|
19970
|
+
...oneshot ? { oneshot } : {}
|
|
18984
19971
|
});
|
|
18985
19972
|
log.debug(`[worktree:add] done branches=${result.branches.join(",")}`);
|
|
18986
19973
|
return jsonResponse({
|
|
@@ -18996,16 +19983,22 @@ async function apiDeleteWorktree(name) {
|
|
|
18996
19983
|
return jsonResponse({ ok: true });
|
|
18997
19984
|
});
|
|
18998
19985
|
}
|
|
18999
|
-
async function apiOpenWorktree(name) {
|
|
19986
|
+
async function apiOpenWorktree(name, req) {
|
|
19000
19987
|
ensureBranchNotBusy(name);
|
|
19001
|
-
|
|
19002
|
-
|
|
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 } : {} });
|
|
19003
19995
|
log.debug(`[worktree:open] done name=${name} worktreeId=${result.worktreeId}`);
|
|
19004
19996
|
return jsonResponse({ ok: true });
|
|
19005
19997
|
}
|
|
19006
19998
|
async function apiCloseWorktree(name) {
|
|
19007
19999
|
ensureBranchNotBusy(name);
|
|
19008
20000
|
log.info(`[worktree:close] name=${name}`);
|
|
20001
|
+
await disarmOneshotIfArmed(name, "close-worktree");
|
|
19009
20002
|
await lifecycleService.closeWorktree(name);
|
|
19010
20003
|
log.debug(`[worktree:close] done name=${name}`);
|
|
19011
20004
|
return jsonResponse({ ok: true });
|
|
@@ -19017,6 +20010,7 @@ async function apiSetWorktreeArchived(name, req) {
|
|
|
19017
20010
|
return parsed.response;
|
|
19018
20011
|
const body = parsed.data;
|
|
19019
20012
|
log.info(`[worktree:archive] name=${name} archived=${body.archived}`);
|
|
20013
|
+
await disarmOneshotIfArmed(name, "archive-worktree");
|
|
19020
20014
|
await lifecycleService.setWorktreeArchived(name, body.archived);
|
|
19021
20015
|
log.debug(`[worktree:archive] done name=${name} archived=${body.archived}`);
|
|
19022
20016
|
return jsonResponse({ ok: true, archived: body.archived });
|
|
@@ -19041,6 +20035,7 @@ async function apiSendPrompt(name, req) {
|
|
|
19041
20035
|
const text = body.text;
|
|
19042
20036
|
const preamble = body.preamble;
|
|
19043
20037
|
log.info(`[worktree:send] name=${name} text="${text.slice(0, 80)}"`);
|
|
20038
|
+
await disarmOneshotIfArmed(name, "send-prompt");
|
|
19044
20039
|
const terminalWorktree = await resolveTerminalWorktree(name);
|
|
19045
20040
|
const submitDelayMs = resolveWorktreeTerminalSubmitDelayMs(terminalWorktree.agentName);
|
|
19046
20041
|
const result = await sendPrompt(terminalWorktree.worktreeId, terminalWorktree.attachTarget, text, 0, preamble, submitDelayMs);
|
|
@@ -19051,6 +20046,7 @@ async function apiSendPrompt(name, req) {
|
|
|
19051
20046
|
async function apiMergeWorktree(name) {
|
|
19052
20047
|
ensureBranchNotBusy(name);
|
|
19053
20048
|
log.info(`[worktree:merge] name=${name}`);
|
|
20049
|
+
await disarmOneshotIfArmed(name, "merge-worktree");
|
|
19054
20050
|
await lifecycleService.mergeWorktree(name);
|
|
19055
20051
|
log.debug(`[worktree:merge] done name=${name}`);
|
|
19056
20052
|
return jsonResponse({ ok: true });
|
|
@@ -19174,6 +20170,74 @@ async function apiPullMain(req) {
|
|
|
19174
20170
|
log.info(`[pull-main] ${repo || "main"} ${force ? "force " : ""}pull: ${result.status}`);
|
|
19175
20171
|
return jsonResponse(result);
|
|
19176
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
|
+
}
|
|
19177
20241
|
async function apiGetLinearIssues() {
|
|
19178
20242
|
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
19179
20243
|
const fetchResult = config.integrations.linear.enabled && apiKey?.trim() ? await fetchAssignedIssues() : undefined;
|
|
@@ -19231,6 +20295,7 @@ async function apiUploadFiles(name, req) {
|
|
|
19231
20295
|
const state = projectRuntime.getWorktreeByBranch(name);
|
|
19232
20296
|
if (!state)
|
|
19233
20297
|
return errorResponse(`Worktree not found: ${name}`, 404);
|
|
20298
|
+
await disarmOneshotIfArmed(name, "upload-files");
|
|
19234
20299
|
let formData;
|
|
19235
20300
|
try {
|
|
19236
20301
|
formData = await req.formData();
|
|
@@ -19253,7 +20318,7 @@ async function apiUploadFiles(name, req) {
|
|
|
19253
20318
|
return errorResponse(`File too large: ${entry.name} (max 10MB)`, 400);
|
|
19254
20319
|
}
|
|
19255
20320
|
const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
|
|
19256
|
-
const destPath =
|
|
20321
|
+
const destPath = join7(uploadDir, safeName);
|
|
19257
20322
|
if (!resolve9(destPath).startsWith(uploadDir + "/")) {
|
|
19258
20323
|
return errorResponse("Invalid filename", 400);
|
|
19259
20324
|
}
|
|
@@ -19417,7 +20482,7 @@ Bun.serve({
|
|
|
19417
20482
|
if (!parsed.ok)
|
|
19418
20483
|
return parsed.response;
|
|
19419
20484
|
const name = parsed.data;
|
|
19420
|
-
return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name));
|
|
20485
|
+
return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name, req));
|
|
19421
20486
|
}
|
|
19422
20487
|
},
|
|
19423
20488
|
"/api/worktrees/:name/terminal-launch": {
|
|
@@ -19447,6 +20512,24 @@ Bun.serve({
|
|
|
19447
20512
|
return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
|
|
19448
20513
|
}
|
|
19449
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
|
+
},
|
|
19450
20533
|
[apiPaths.setWorktreeLabel]: {
|
|
19451
20534
|
PUT: (req) => {
|
|
19452
20535
|
const parsed = parseWorktreeNameParam(req.params);
|
|
@@ -19531,7 +20614,7 @@ Bun.serve({
|
|
|
19531
20614
|
const url = new URL(req.url);
|
|
19532
20615
|
if (STATIC_DIR) {
|
|
19533
20616
|
const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
|
|
19534
|
-
const filePath =
|
|
20617
|
+
const filePath = join7(STATIC_DIR, rawPath);
|
|
19535
20618
|
const staticRoot = resolve9(STATIC_DIR);
|
|
19536
20619
|
if (!resolve9(filePath).startsWith(staticRoot + "/")) {
|
|
19537
20620
|
return new Response("Forbidden", { status: 403 });
|
|
@@ -19541,7 +20624,7 @@ Bun.serve({
|
|
|
19541
20624
|
const headers = rawPath.startsWith("/assets/") ? { "Cache-Control": "public, max-age=31536000, immutable" } : {};
|
|
19542
20625
|
return new Response(file, { headers });
|
|
19543
20626
|
}
|
|
19544
|
-
return new Response(Bun.file(
|
|
20627
|
+
return new Response(Bun.file(join7(STATIC_DIR, "index.html")), {
|
|
19545
20628
|
headers: { "Cache-Control": "no-cache" }
|
|
19546
20629
|
});
|
|
19547
20630
|
}
|
|
@@ -19577,6 +20660,9 @@ Bun.serve({
|
|
|
19577
20660
|
const attachId = getAttachedSessionId(data, ws);
|
|
19578
20661
|
if (!attachId)
|
|
19579
20662
|
return;
|
|
20663
|
+
if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
|
|
20664
|
+
disarmOneshotIfArmed(branch, "terminal-ws-input");
|
|
20665
|
+
}
|
|
19580
20666
|
write(attachId, msg.data);
|
|
19581
20667
|
break;
|
|
19582
20668
|
}
|
|
@@ -19584,6 +20670,9 @@ Bun.serve({
|
|
|
19584
20670
|
const attachId = getAttachedSessionId(data, ws);
|
|
19585
20671
|
if (!attachId)
|
|
19586
20672
|
return;
|
|
20673
|
+
if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
|
|
20674
|
+
disarmOneshotIfArmed(branch, "terminal-ws-send-keys");
|
|
20675
|
+
}
|
|
19587
20676
|
await sendKeys(attachId, msg.hexBytes);
|
|
19588
20677
|
break;
|
|
19589
20678
|
}
|
|
@@ -19664,6 +20753,15 @@ startPrMonitor(getWorktreeGitDirs, config.integrations.github.linkedRepos, PROJE
|
|
|
19664
20753
|
if (linearAutoCreateEnabled) {
|
|
19665
20754
|
startLinearAutoCreate();
|
|
19666
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
|
+
});
|
|
19667
20765
|
if (config.workspace.autoPull.enabled) {
|
|
19668
20766
|
startAutoPullMonitor({ git, projectRoot: PROJECT_DIR, mainBranch: config.workspace.mainBranch }, config.workspace.autoPull.intervalSeconds * 1000);
|
|
19669
20767
|
}
|