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/bin/webmux.js
CHANGED
|
@@ -50,12 +50,26 @@ var __require = import.meta.require;
|
|
|
50
50
|
// backend/src/adapters/git.ts
|
|
51
51
|
import { readdirSync, rmSync, statSync } from "fs";
|
|
52
52
|
import { resolve, join } from "path";
|
|
53
|
+
function spawnGit(args, cwd) {
|
|
54
|
+
try {
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
result: Bun.spawnSync(["git", ...args], {
|
|
58
|
+
cwd,
|
|
59
|
+
stdout: "pipe",
|
|
60
|
+
stderr: "pipe"
|
|
61
|
+
})
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return { ok: false, stderr: `spawn error (cwd=${cwd}): ${errorMessage(error)}` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
53
67
|
function runGit(args, cwd) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
68
|
+
const spawned = spawnGit(args, cwd);
|
|
69
|
+
if (!spawned.ok) {
|
|
70
|
+
throw new Error(`git ${args.join(" ")} failed: ${spawned.stderr}`);
|
|
71
|
+
}
|
|
72
|
+
const { result } = spawned;
|
|
59
73
|
if (result.exitCode !== 0) {
|
|
60
74
|
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
61
75
|
throw new Error(`git ${args.join(" ")} failed: ${stderr || `exit ${result.exitCode}`}`);
|
|
@@ -63,11 +77,11 @@ function runGit(args, cwd) {
|
|
|
63
77
|
return new TextDecoder().decode(result.stdout).trim();
|
|
64
78
|
}
|
|
65
79
|
function tryRunGit(args, cwd) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
80
|
+
const spawned = spawnGit(args, cwd);
|
|
81
|
+
if (!spawned.ok) {
|
|
82
|
+
return { ok: false, stderr: spawned.stderr };
|
|
83
|
+
}
|
|
84
|
+
const { result } = spawned;
|
|
71
85
|
if (result.exitCode !== 0) {
|
|
72
86
|
return {
|
|
73
87
|
ok: false,
|
|
@@ -188,6 +202,16 @@ function listGitWorktrees(cwd) {
|
|
|
188
202
|
const output = runGit(["worktree", "list", "--porcelain"], cwd);
|
|
189
203
|
return parseGitWorktreePorcelain(output);
|
|
190
204
|
}
|
|
205
|
+
function worktreeEntryPathExists(entry) {
|
|
206
|
+
try {
|
|
207
|
+
return statSync(entry.path).isDirectory();
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function filterLiveWorktreeEntries(entries) {
|
|
213
|
+
return entries.filter(worktreeEntryPathExists);
|
|
214
|
+
}
|
|
191
215
|
function listLocalGitBranches(cwd) {
|
|
192
216
|
const output = runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd);
|
|
193
217
|
return output.split(`
|
|
@@ -248,6 +272,9 @@ class BunGitGateway {
|
|
|
248
272
|
listWorktrees(cwd) {
|
|
249
273
|
return listGitWorktrees(cwd);
|
|
250
274
|
}
|
|
275
|
+
listLiveWorktrees(cwd) {
|
|
276
|
+
return filterLiveWorktreeEntries(listGitWorktrees(cwd));
|
|
277
|
+
}
|
|
251
278
|
listLocalBranches(cwd) {
|
|
252
279
|
return listLocalGitBranches(cwd);
|
|
253
280
|
}
|
|
@@ -435,6 +462,7 @@ _webmux() {
|
|
|
435
462
|
'service:Manage webmux as a system service'
|
|
436
463
|
'update:Update webmux to the latest version'
|
|
437
464
|
'add:Create a worktree'
|
|
465
|
+
'oneshot:Run a worktree start-to-finish, streaming logs to stdout'
|
|
438
466
|
'list:List worktrees and their status'
|
|
439
467
|
'open:Open an existing worktree session'
|
|
440
468
|
'close:Close a worktree session'
|
|
@@ -445,6 +473,7 @@ _webmux() {
|
|
|
445
473
|
'merge:Merge a worktree into main'
|
|
446
474
|
'send:Send a prompt to a running worktree agent'
|
|
447
475
|
'prune:Remove all worktrees in the current project'
|
|
476
|
+
'linear:Post a worktree conversation to a Linear issue/team'
|
|
448
477
|
'completion:Generate shell completion script'
|
|
449
478
|
)
|
|
450
479
|
|
|
@@ -463,6 +492,19 @@ _webmux() {
|
|
|
463
492
|
fi
|
|
464
493
|
fi
|
|
465
494
|
;;
|
|
495
|
+
linear)
|
|
496
|
+
if (( CURRENT == 3 )); then
|
|
497
|
+
local -a subs
|
|
498
|
+
subs=('post:Post a worktree conversation to a Linear issue or team')
|
|
499
|
+
_describe 'linear subcommand' subs
|
|
500
|
+
elif (( CURRENT == 4 )) && [[ "\${words[3]}" == "post" ]]; then
|
|
501
|
+
local -a branches
|
|
502
|
+
branches=(\${(f)"$(webmux --completions send 2>/dev/null)"})
|
|
503
|
+
if (( \${#branches} )); then
|
|
504
|
+
_describe 'worktree' branches
|
|
505
|
+
fi
|
|
506
|
+
fi
|
|
507
|
+
;;
|
|
466
508
|
completion)
|
|
467
509
|
if (( CURRENT == 3 )); then
|
|
468
510
|
local -a shells
|
|
@@ -492,7 +534,7 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
|
|
|
492
534
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
493
535
|
|
|
494
536
|
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
495
|
-
COMPREPLY=($(compgen -W "serve init service update add list open close archive unarchive label remove merge send prune completion" -- "\${cur}"))
|
|
537
|
+
COMPREPLY=($(compgen -W "serve init service update add oneshot list open close archive unarchive label remove merge send prune linear completion" -- "\${cur}"))
|
|
496
538
|
return
|
|
497
539
|
fi
|
|
498
540
|
|
|
@@ -504,6 +546,15 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
|
|
|
504
546
|
COMPREPLY=($(compgen -W "\${branches}" -- "\${cur}"))
|
|
505
547
|
fi
|
|
506
548
|
;;
|
|
549
|
+
linear)
|
|
550
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
551
|
+
COMPREPLY=($(compgen -W "post" -- "\${cur}"))
|
|
552
|
+
elif [[ \${COMP_CWORD} -eq 3 ]] && [[ "\${COMP_WORDS[2]}" == "post" ]]; then
|
|
553
|
+
local branches
|
|
554
|
+
branches=$(webmux --completions send 2>/dev/null)
|
|
555
|
+
COMPREPLY=($(compgen -W "\${branches}" -- "\${cur}"))
|
|
556
|
+
fi
|
|
557
|
+
;;
|
|
507
558
|
completion)
|
|
508
559
|
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
509
560
|
COMPREPLY=($(compgen -W "bash zsh" -- "\${cur}"))
|
|
@@ -1810,7 +1861,29 @@ function detectProjectName(gitRoot) {
|
|
|
1810
1861
|
}
|
|
1811
1862
|
return basename2(gitRoot);
|
|
1812
1863
|
}
|
|
1813
|
-
|
|
1864
|
+
function formatServerError(error, port) {
|
|
1865
|
+
if (error instanceof Error) {
|
|
1866
|
+
if (error.message.startsWith("HTTP"))
|
|
1867
|
+
return error.message;
|
|
1868
|
+
if (error.message.includes("fetch")) {
|
|
1869
|
+
return `Could not connect to webmux server on port ${port}. Is it running?`;
|
|
1870
|
+
}
|
|
1871
|
+
return error.message;
|
|
1872
|
+
}
|
|
1873
|
+
return String(error);
|
|
1874
|
+
}
|
|
1875
|
+
async function withServerConnection(port, fn) {
|
|
1876
|
+
try {
|
|
1877
|
+
return await fn();
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
throw new Error(formatServerError(error, port));
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
var CommandUsageError;
|
|
1883
|
+
var init_shared = __esm(() => {
|
|
1884
|
+
CommandUsageError = class CommandUsageError extends Error {
|
|
1885
|
+
};
|
|
1886
|
+
});
|
|
1814
1887
|
|
|
1815
1888
|
// bin/src/init-helpers.ts
|
|
1816
1889
|
import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
|
|
@@ -6943,7 +7016,7 @@ var init_zod = __esm(() => {
|
|
|
6943
7016
|
init_external();
|
|
6944
7017
|
});
|
|
6945
7018
|
|
|
6946
|
-
// node_modules/.bun/@ts-rest+core@3.52.1+
|
|
7019
|
+
// node_modules/.bun/@ts-rest+core@3.52.1+596964f7fee2c930/node_modules/@ts-rest/core/index.esm.mjs
|
|
6947
7020
|
var isZodType = (obj) => {
|
|
6948
7021
|
return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
|
|
6949
7022
|
}, isZodObjectStrict = (obj) => {
|
|
@@ -7257,7 +7330,15 @@ var init_index_esm = __esm(() => {
|
|
|
7257
7330
|
});
|
|
7258
7331
|
|
|
7259
7332
|
// packages/api-contract/src/schemas.ts
|
|
7260
|
-
|
|
7333
|
+
function parseLinearTarget(raw) {
|
|
7334
|
+
const trimmed = raw.trim();
|
|
7335
|
+
if (LinearIssueIdSchema.safeParse(trimmed).success)
|
|
7336
|
+
return { kind: "issue", issueId: trimmed };
|
|
7337
|
+
if (LinearTeamKeySchema.safeParse(trimmed).success)
|
|
7338
|
+
return { kind: "team", teamKey: trimmed };
|
|
7339
|
+
return { kind: "invalid", raw: trimmed };
|
|
7340
|
+
}
|
|
7341
|
+
var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, BuiltInAgentIdSchema, AgentIdSchema, WorktreeCreateModeSchema, LinearIssueIdSchema, LinearTeamKeySchema, PostWorktreeToLinearTargetSchema, PostWorktreeToLinearRequestSchema, PostWorktreeToLinearResponseSchema, FromLinearInputSchema, OneshotConfigSchema, AgentCapabilitiesSchema, AgentSummarySchema, AgentDetailsSchema, AgentListResponseSchema, UpsertCustomAgentRequestSchema, AgentResponseSchema, ValidateCustomAgentResponseSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, WorktreeSourceSchema, CreateWorktreeRequestSchema, OpenWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, SetWorktreeLabelRequestSchema, SetWorktreeLabelResponseSchema, ToggleEnabledRequestSchema, SendWorktreePromptRequestSchema, AgentsSendMessageRequestSchema, PullMainRequestSchema, PullMainStatusSchema, PullMainResponseSchema, ServiceStatusSchema, PrCommentSchema, CiCheckSchema, PrEntrySchema, LinearIssueLabelSchema, LinearIssueStateSchema, LinkedLinearIssueSchema, LinearIssueSchema, LinearIssueAvailabilitySchema, LinearIssuesResponseSchema, WorktreeCreationStateSchema, AppNotificationSchema, ProjectWorktreeSnapshotSchema, ProjectSnapshotSchema, WorktreeConversationProviderSchema, CodexWorktreeConversationRefSchema, ClaudeWorktreeConversationRefSchema, WorktreeConversationRefSchema, AgentsUiWorktreeSummarySchema, AgentsUiConversationMessageRoleSchema, AgentsUiConversationMessageStatusSchema, AgentsUiConversationMessageKindSchema, AgentsUiConversationMessageSchema, AgentsUiConversationStateSchema, AgentsUiWorktreeConversationResponseSchema, AgentsUiSendMessageResponseSchema, AgentsUiInterruptResponseSchema, AgentsUiConversationSnapshotEventSchema, AgentsUiConversationMessageDeltaEventSchema, AgentsUiConversationErrorEventSchema, AgentsUiConversationEventSchema, WorktreeListResponseSchema, UnpushedCommitSchema, WorktreeDiffResponseSchema, ServiceConfigSchema, ProfileConfigSchema, LinkedRepoInfoSchema, AppConfigSchema, CiLogsResponseSchema, WorktreeNameParamsSchema, NotificationIdParamsSchema, AgentIdParamsSchema, RunIdParamsSchema;
|
|
7261
7342
|
var init_schemas = __esm(() => {
|
|
7262
7343
|
init_zod();
|
|
7263
7344
|
BooleanLikeSchema = exports_external.union([
|
|
@@ -7278,6 +7359,30 @@ var init_schemas = __esm(() => {
|
|
|
7278
7359
|
BuiltInAgentIdSchema = exports_external.enum(["claude", "codex"]);
|
|
7279
7360
|
AgentIdSchema = exports_external.string().trim().min(1);
|
|
7280
7361
|
WorktreeCreateModeSchema = exports_external.enum(["new", "existing"]);
|
|
7362
|
+
LinearIssueIdSchema = exports_external.string().regex(/^[A-Z]+-\d+$/, "Expected Linear issue id (e.g. ENG-123)");
|
|
7363
|
+
LinearTeamKeySchema = exports_external.string().regex(/^[A-Z]+$/, "Expected Linear team key (e.g. ENG)");
|
|
7364
|
+
PostWorktreeToLinearTargetSchema = exports_external.discriminatedUnion("kind", [
|
|
7365
|
+
exports_external.object({ kind: exports_external.literal("issue"), issueId: LinearIssueIdSchema }),
|
|
7366
|
+
exports_external.object({ kind: exports_external.literal("team"), teamKey: LinearTeamKeySchema, title: exports_external.string().trim().min(1).optional() })
|
|
7367
|
+
]);
|
|
7368
|
+
PostWorktreeToLinearRequestSchema = exports_external.object({
|
|
7369
|
+
target: PostWorktreeToLinearTargetSchema
|
|
7370
|
+
});
|
|
7371
|
+
PostWorktreeToLinearResponseSchema = exports_external.object({
|
|
7372
|
+
ok: exports_external.literal(true),
|
|
7373
|
+
issueId: exports_external.string(),
|
|
7374
|
+
issueUrl: exports_external.string(),
|
|
7375
|
+
commentUrl: exports_external.string().nullable(),
|
|
7376
|
+
attachmentUrl: exports_external.string()
|
|
7377
|
+
});
|
|
7378
|
+
FromLinearInputSchema = exports_external.object({
|
|
7379
|
+
issueId: LinearIssueIdSchema,
|
|
7380
|
+
conversationContext: exports_external.string().optional()
|
|
7381
|
+
});
|
|
7382
|
+
OneshotConfigSchema = exports_external.object({
|
|
7383
|
+
autoCloseOnDone: exports_external.boolean().optional(),
|
|
7384
|
+
postToLinearOnDone: PostWorktreeToLinearTargetSchema.optional()
|
|
7385
|
+
});
|
|
7281
7386
|
AgentCapabilitiesSchema = exports_external.object({
|
|
7282
7387
|
terminal: exports_external.literal(true),
|
|
7283
7388
|
inAppChat: exports_external.boolean(),
|
|
@@ -7334,6 +7439,7 @@ var init_schemas = __esm(() => {
|
|
|
7334
7439
|
BranchListResponseSchema = exports_external.object({
|
|
7335
7440
|
branches: exports_external.array(AvailableBranchSchema)
|
|
7336
7441
|
});
|
|
7442
|
+
WorktreeSourceSchema = exports_external.enum(["ui", "oneshot"]);
|
|
7337
7443
|
CreateWorktreeRequestSchema = exports_external.object({
|
|
7338
7444
|
mode: WorktreeCreateModeSchema.optional(),
|
|
7339
7445
|
branch: exports_external.string().optional(),
|
|
@@ -7344,7 +7450,14 @@ var init_schemas = __esm(() => {
|
|
|
7344
7450
|
prompt: exports_external.string().optional(),
|
|
7345
7451
|
envOverrides: exports_external.record(exports_external.string()).optional(),
|
|
7346
7452
|
createLinearTicket: exports_external.literal(true).optional(),
|
|
7347
|
-
linearTitle: exports_external.string().optional()
|
|
7453
|
+
linearTitle: exports_external.string().optional(),
|
|
7454
|
+
fromLinear: FromLinearInputSchema.optional(),
|
|
7455
|
+
source: WorktreeSourceSchema.optional(),
|
|
7456
|
+
oneshot: OneshotConfigSchema.optional()
|
|
7457
|
+
});
|
|
7458
|
+
OpenWorktreeRequestSchema = exports_external.object({
|
|
7459
|
+
prompt: exports_external.string().optional(),
|
|
7460
|
+
oneshot: OneshotConfigSchema.optional()
|
|
7348
7461
|
});
|
|
7349
7462
|
CreateWorktreeResponseSchema = exports_external.object({
|
|
7350
7463
|
primaryBranch: exports_external.string(),
|
|
@@ -7490,7 +7603,9 @@ var init_schemas = __esm(() => {
|
|
|
7490
7603
|
services: exports_external.array(ServiceStatusSchema),
|
|
7491
7604
|
prs: exports_external.array(PrEntrySchema),
|
|
7492
7605
|
linearIssue: LinkedLinearIssueSchema.nullable(),
|
|
7493
|
-
creation: WorktreeCreationStateSchema.nullable()
|
|
7606
|
+
creation: WorktreeCreationStateSchema.nullable(),
|
|
7607
|
+
source: WorktreeSourceSchema,
|
|
7608
|
+
oneshot: OneshotConfigSchema.nullable()
|
|
7494
7609
|
});
|
|
7495
7610
|
ProjectSnapshotSchema = exports_external.object({
|
|
7496
7611
|
project: exports_external.object({
|
|
@@ -7539,13 +7654,16 @@ var init_schemas = __esm(() => {
|
|
|
7539
7654
|
});
|
|
7540
7655
|
AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
|
|
7541
7656
|
AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
|
|
7657
|
+
AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
|
|
7542
7658
|
AgentsUiConversationMessageSchema = exports_external.object({
|
|
7543
7659
|
id: exports_external.string(),
|
|
7544
7660
|
turnId: exports_external.string(),
|
|
7545
7661
|
role: AgentsUiConversationMessageRoleSchema,
|
|
7546
7662
|
text: exports_external.string(),
|
|
7547
7663
|
status: AgentsUiConversationMessageStatusSchema,
|
|
7548
|
-
createdAt: exports_external.string().nullable()
|
|
7664
|
+
createdAt: exports_external.string().nullable(),
|
|
7665
|
+
kind: AgentsUiConversationMessageKindSchema.optional(),
|
|
7666
|
+
toolName: exports_external.string().optional()
|
|
7549
7667
|
});
|
|
7550
7668
|
AgentsUiConversationStateSchema = exports_external.object({
|
|
7551
7669
|
provider: WorktreeConversationProviderSchema,
|
|
@@ -7674,6 +7792,8 @@ var init_contract = __esm(() => {
|
|
|
7674
7792
|
openWorktree: "/api/worktrees/:name/open",
|
|
7675
7793
|
closeWorktree: "/api/worktrees/:name/close",
|
|
7676
7794
|
setWorktreeArchived: "/api/worktrees/:name/archive",
|
|
7795
|
+
syncWorktreePrs: "/api/worktrees/:name/sync-prs",
|
|
7796
|
+
postWorktreeToLinear: "/api/worktrees/:name/linear/post",
|
|
7677
7797
|
setWorktreeLabel: "/api/worktrees/:name/label",
|
|
7678
7798
|
sendWorktreePrompt: "/api/worktrees/:name/send",
|
|
7679
7799
|
mergeWorktree: "/api/worktrees/:name/merge",
|
|
@@ -7852,7 +7972,7 @@ var init_contract = __esm(() => {
|
|
|
7852
7972
|
method: "POST",
|
|
7853
7973
|
path: apiPaths.openWorktree,
|
|
7854
7974
|
pathParams: WorktreeNameParamsSchema,
|
|
7855
|
-
body:
|
|
7975
|
+
body: OpenWorktreeRequestSchema,
|
|
7856
7976
|
responses: {
|
|
7857
7977
|
200: OkResponseSchema,
|
|
7858
7978
|
...commonErrorResponses
|
|
@@ -7878,6 +7998,26 @@ var init_contract = __esm(() => {
|
|
|
7878
7998
|
...commonErrorResponses
|
|
7879
7999
|
}
|
|
7880
8000
|
},
|
|
8001
|
+
postWorktreeToLinear: {
|
|
8002
|
+
method: "POST",
|
|
8003
|
+
path: apiPaths.postWorktreeToLinear,
|
|
8004
|
+
pathParams: WorktreeNameParamsSchema,
|
|
8005
|
+
body: PostWorktreeToLinearRequestSchema,
|
|
8006
|
+
responses: {
|
|
8007
|
+
200: PostWorktreeToLinearResponseSchema,
|
|
8008
|
+
...commonErrorResponses
|
|
8009
|
+
}
|
|
8010
|
+
},
|
|
8011
|
+
syncWorktreePrs: {
|
|
8012
|
+
method: "POST",
|
|
8013
|
+
path: apiPaths.syncWorktreePrs,
|
|
8014
|
+
pathParams: WorktreeNameParamsSchema,
|
|
8015
|
+
body: c.noBody(),
|
|
8016
|
+
responses: {
|
|
8017
|
+
200: ProjectWorktreeSnapshotSchema,
|
|
8018
|
+
...commonErrorResponses
|
|
8019
|
+
}
|
|
8020
|
+
},
|
|
7881
8021
|
setWorktreeLabel: {
|
|
7882
8022
|
method: "PUT",
|
|
7883
8023
|
path: apiPaths.setWorktreeLabel,
|
|
@@ -7935,128 +8075,1464 @@ var init_contract = __esm(() => {
|
|
|
7935
8075
|
...commonErrorResponses
|
|
7936
8076
|
}
|
|
7937
8077
|
},
|
|
7938
|
-
setAutoRemoveOnMerge: {
|
|
7939
|
-
method: "PUT",
|
|
7940
|
-
path: apiPaths.setAutoRemoveOnMerge,
|
|
7941
|
-
body: ToggleEnabledRequestSchema,
|
|
7942
|
-
responses: {
|
|
7943
|
-
200: EnabledResponseSchema,
|
|
7944
|
-
...commonErrorResponses
|
|
7945
|
-
}
|
|
8078
|
+
setAutoRemoveOnMerge: {
|
|
8079
|
+
method: "PUT",
|
|
8080
|
+
path: apiPaths.setAutoRemoveOnMerge,
|
|
8081
|
+
body: ToggleEnabledRequestSchema,
|
|
8082
|
+
responses: {
|
|
8083
|
+
200: EnabledResponseSchema,
|
|
8084
|
+
...commonErrorResponses
|
|
8085
|
+
}
|
|
8086
|
+
},
|
|
8087
|
+
pullMain: {
|
|
8088
|
+
method: "POST",
|
|
8089
|
+
path: apiPaths.pullMain,
|
|
8090
|
+
body: PullMainRequestSchema,
|
|
8091
|
+
responses: {
|
|
8092
|
+
200: PullMainResponseSchema,
|
|
8093
|
+
...commonErrorResponses
|
|
8094
|
+
}
|
|
8095
|
+
},
|
|
8096
|
+
fetchCiLogs: {
|
|
8097
|
+
method: "GET",
|
|
8098
|
+
path: apiPaths.fetchCiLogs,
|
|
8099
|
+
pathParams: RunIdParamsSchema,
|
|
8100
|
+
responses: {
|
|
8101
|
+
200: CiLogsResponseSchema,
|
|
8102
|
+
...commonErrorResponses
|
|
8103
|
+
}
|
|
8104
|
+
},
|
|
8105
|
+
dismissNotification: {
|
|
8106
|
+
method: "POST",
|
|
8107
|
+
path: apiPaths.dismissNotification,
|
|
8108
|
+
pathParams: NotificationIdParamsSchema,
|
|
8109
|
+
body: c.noBody(),
|
|
8110
|
+
responses: {
|
|
8111
|
+
200: OkResponseSchema,
|
|
8112
|
+
400: ErrorResponseSchema,
|
|
8113
|
+
404: ErrorResponseSchema
|
|
8114
|
+
}
|
|
8115
|
+
}
|
|
8116
|
+
}, {
|
|
8117
|
+
strictStatusCodes: true
|
|
8118
|
+
});
|
|
8119
|
+
});
|
|
8120
|
+
|
|
8121
|
+
// packages/api-contract/src/client.ts
|
|
8122
|
+
function createApiClient(baseUrl, options = {}) {
|
|
8123
|
+
return initClient(apiContract, {
|
|
8124
|
+
baseUrl,
|
|
8125
|
+
throwOnUnknownStatus: true,
|
|
8126
|
+
baseHeaders: {},
|
|
8127
|
+
...options
|
|
8128
|
+
});
|
|
8129
|
+
}
|
|
8130
|
+
function isRecord2(value) {
|
|
8131
|
+
return typeof value === "object" && value !== null;
|
|
8132
|
+
}
|
|
8133
|
+
function isRouteResponse(value) {
|
|
8134
|
+
return isRecord2(value) && "status" in value && typeof value.status === "number" && "body" in value;
|
|
8135
|
+
}
|
|
8136
|
+
function unwrapResponse(response) {
|
|
8137
|
+
if (!isRouteResponse(response)) {
|
|
8138
|
+
throw new Error("Malformed API client response");
|
|
8139
|
+
}
|
|
8140
|
+
if (response.status < 200 || response.status >= 300) {
|
|
8141
|
+
throw new Error(errorMessageFromResponse(response.body, response.status));
|
|
8142
|
+
}
|
|
8143
|
+
return response.body;
|
|
8144
|
+
}
|
|
8145
|
+
function errorMessageFromResponse(body, status2) {
|
|
8146
|
+
if (typeof body === "string") {
|
|
8147
|
+
try {
|
|
8148
|
+
const parsed = JSON.parse(body);
|
|
8149
|
+
return errorMessageFromResponse(parsed, status2);
|
|
8150
|
+
} catch {
|
|
8151
|
+
return body.trim() || `HTTP ${status2}`;
|
|
8152
|
+
}
|
|
8153
|
+
}
|
|
8154
|
+
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
|
8155
|
+
return body.error;
|
|
8156
|
+
}
|
|
8157
|
+
return `HTTP ${status2}`;
|
|
8158
|
+
}
|
|
8159
|
+
function withEncodedPathParams(args) {
|
|
8160
|
+
const [first, ...rest] = args;
|
|
8161
|
+
if (!first || typeof first !== "object" || !("params" in first) || !first.params || typeof first.params !== "object") {
|
|
8162
|
+
return args;
|
|
8163
|
+
}
|
|
8164
|
+
const encodedParams = Object.fromEntries(Object.entries(first.params).map(([key, value]) => [key, encodeURIComponent(String(value))]));
|
|
8165
|
+
return [
|
|
8166
|
+
{
|
|
8167
|
+
...first,
|
|
8168
|
+
params: encodedParams
|
|
8169
|
+
},
|
|
8170
|
+
...rest
|
|
8171
|
+
];
|
|
8172
|
+
}
|
|
8173
|
+
function wrapRouteCall(routeCall) {
|
|
8174
|
+
return async (...args) => unwrapResponse(await routeCall(...withEncodedPathParams(args)));
|
|
8175
|
+
}
|
|
8176
|
+
function wrapClient(client) {
|
|
8177
|
+
return Object.fromEntries(Object.entries(client).map(([key, value]) => {
|
|
8178
|
+
if (typeof value === "function") {
|
|
8179
|
+
return [key, wrapRouteCall((...args) => Promise.resolve(Reflect.apply(value, undefined, args)))];
|
|
8180
|
+
}
|
|
8181
|
+
if (isRecord2(value)) {
|
|
8182
|
+
return [key, wrapClient(value)];
|
|
8183
|
+
}
|
|
8184
|
+
return [key, value];
|
|
8185
|
+
}));
|
|
8186
|
+
}
|
|
8187
|
+
function createApi(baseUrl, options = {}) {
|
|
8188
|
+
return wrapClient(createApiClient(baseUrl, options));
|
|
8189
|
+
}
|
|
8190
|
+
var init_client = __esm(() => {
|
|
8191
|
+
init_index_esm();
|
|
8192
|
+
init_contract();
|
|
8193
|
+
});
|
|
8194
|
+
|
|
8195
|
+
// packages/api-contract/src/index.ts
|
|
8196
|
+
var init_src = __esm(() => {
|
|
8197
|
+
init_contract();
|
|
8198
|
+
init_client();
|
|
8199
|
+
init_schemas();
|
|
8200
|
+
});
|
|
8201
|
+
|
|
8202
|
+
// backend/src/lib/log.ts
|
|
8203
|
+
function ts() {
|
|
8204
|
+
return new Date().toISOString().slice(11, 23);
|
|
8205
|
+
}
|
|
8206
|
+
var DEBUG, log;
|
|
8207
|
+
var init_log = __esm(() => {
|
|
8208
|
+
DEBUG = Bun.env.WEBMUX_DEBUG === "1";
|
|
8209
|
+
log = {
|
|
8210
|
+
info(msg) {
|
|
8211
|
+
console.log(`[${ts()}] ${msg}`);
|
|
8212
|
+
},
|
|
8213
|
+
debug(msg) {
|
|
8214
|
+
if (DEBUG)
|
|
8215
|
+
console.log(`[${ts()}] ${msg}`);
|
|
8216
|
+
},
|
|
8217
|
+
warn(msg) {
|
|
8218
|
+
console.warn(`[${ts()}] ${msg}`);
|
|
8219
|
+
},
|
|
8220
|
+
error(msg, err) {
|
|
8221
|
+
err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
|
|
8222
|
+
}
|
|
8223
|
+
};
|
|
8224
|
+
});
|
|
8225
|
+
|
|
8226
|
+
// backend/src/services/linear-service.ts
|
|
8227
|
+
function gqlErrorMessage(raw) {
|
|
8228
|
+
return raw.errors && raw.errors.length > 0 ? raw.errors.map((error) => error.message).join("; ") : null;
|
|
8229
|
+
}
|
|
8230
|
+
function parseViewerIdResponse(raw) {
|
|
8231
|
+
const error = gqlErrorMessage(raw);
|
|
8232
|
+
if (error) {
|
|
8233
|
+
return { ok: false, error };
|
|
8234
|
+
}
|
|
8235
|
+
const viewerId = raw.data?.viewer.id;
|
|
8236
|
+
if (!viewerId) {
|
|
8237
|
+
return { ok: false, error: "No viewer id in response" };
|
|
8238
|
+
}
|
|
8239
|
+
return { ok: true, data: viewerId };
|
|
8240
|
+
}
|
|
8241
|
+
function parseInProgressStateIdResponse(raw) {
|
|
8242
|
+
const error = gqlErrorMessage(raw);
|
|
8243
|
+
if (error) {
|
|
8244
|
+
return { ok: false, error };
|
|
8245
|
+
}
|
|
8246
|
+
const states = raw.data?.team?.states.nodes;
|
|
8247
|
+
if (!states) {
|
|
8248
|
+
return { ok: false, error: "No team states in response" };
|
|
8249
|
+
}
|
|
8250
|
+
const preferredState = states.find((state) => state.type === "started" && state.name.trim().toLowerCase() === "in progress");
|
|
8251
|
+
if (preferredState) {
|
|
8252
|
+
return { ok: true, data: preferredState.id };
|
|
8253
|
+
}
|
|
8254
|
+
const startedState = states.find((state) => state.type === "started");
|
|
8255
|
+
if (!startedState) {
|
|
8256
|
+
return { ok: false, error: "No started workflow state found for team" };
|
|
8257
|
+
}
|
|
8258
|
+
return { ok: true, data: startedState.id };
|
|
8259
|
+
}
|
|
8260
|
+
function parseIssueCreateResponse(raw) {
|
|
8261
|
+
const error = gqlErrorMessage(raw);
|
|
8262
|
+
if (error) {
|
|
8263
|
+
return { ok: false, error };
|
|
8264
|
+
}
|
|
8265
|
+
const payload = raw.data?.issueCreate;
|
|
8266
|
+
if (!payload) {
|
|
8267
|
+
return { ok: false, error: "No issueCreate payload in response" };
|
|
8268
|
+
}
|
|
8269
|
+
if (!payload.success || !payload.issue) {
|
|
8270
|
+
return { ok: false, error: "Linear issue creation was not successful" };
|
|
8271
|
+
}
|
|
8272
|
+
if (!payload.issue.branchName) {
|
|
8273
|
+
return { ok: false, error: "Linear issue did not return a branch name" };
|
|
8274
|
+
}
|
|
8275
|
+
return {
|
|
8276
|
+
ok: true,
|
|
8277
|
+
data: {
|
|
8278
|
+
id: payload.issue.id,
|
|
8279
|
+
identifier: payload.issue.identifier,
|
|
8280
|
+
title: payload.issue.title,
|
|
8281
|
+
url: payload.issue.url,
|
|
8282
|
+
branchName: payload.issue.branchName
|
|
8283
|
+
}
|
|
8284
|
+
};
|
|
8285
|
+
}
|
|
8286
|
+
async function postLinearGraphql(query, variables) {
|
|
8287
|
+
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
8288
|
+
if (!apiKey) {
|
|
8289
|
+
return { ok: false, error: "LINEAR_API_KEY not set" };
|
|
8290
|
+
}
|
|
8291
|
+
try {
|
|
8292
|
+
const res = await fetch("https://api.linear.app/graphql", {
|
|
8293
|
+
method: "POST",
|
|
8294
|
+
headers: {
|
|
8295
|
+
"Content-Type": "application/json",
|
|
8296
|
+
Authorization: apiKey
|
|
8297
|
+
},
|
|
8298
|
+
body: JSON.stringify(variables ? { query, variables } : { query })
|
|
8299
|
+
});
|
|
8300
|
+
if (!res.ok) {
|
|
8301
|
+
const text = await res.text();
|
|
8302
|
+
return { ok: false, error: `Linear API ${res.status}: ${text.slice(0, 200)}` };
|
|
8303
|
+
}
|
|
8304
|
+
return {
|
|
8305
|
+
ok: true,
|
|
8306
|
+
data: await res.json()
|
|
8307
|
+
};
|
|
8308
|
+
} catch (err) {
|
|
8309
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8310
|
+
return { ok: false, error: msg };
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
async function fetchViewerId() {
|
|
8314
|
+
if (viewerIdCache) {
|
|
8315
|
+
return { ok: true, data: viewerIdCache };
|
|
8316
|
+
}
|
|
8317
|
+
const response = await postLinearGraphql(VIEWER_QUERY);
|
|
8318
|
+
if (!response.ok) {
|
|
8319
|
+
log.error(`[linear] viewer fetch failed: ${response.error}`);
|
|
8320
|
+
return { ok: false, error: response.error };
|
|
8321
|
+
}
|
|
8322
|
+
const result = parseViewerIdResponse(response.data);
|
|
8323
|
+
if (!result.ok) {
|
|
8324
|
+
log.error(`[linear] viewer GraphQL error: ${result.error}`);
|
|
8325
|
+
return result;
|
|
8326
|
+
}
|
|
8327
|
+
viewerIdCache = result.data;
|
|
8328
|
+
return result;
|
|
8329
|
+
}
|
|
8330
|
+
async function fetchInProgressStateId(teamId) {
|
|
8331
|
+
const cachedStateId = inProgressStateIdCache.get(teamId);
|
|
8332
|
+
if (cachedStateId) {
|
|
8333
|
+
return { ok: true, data: cachedStateId };
|
|
8334
|
+
}
|
|
8335
|
+
const response = await postLinearGraphql(TEAM_STATES_QUERY, { teamId });
|
|
8336
|
+
if (!response.ok) {
|
|
8337
|
+
log.error(`[linear] team states fetch failed: ${response.error}`);
|
|
8338
|
+
return { ok: false, error: response.error };
|
|
8339
|
+
}
|
|
8340
|
+
const result = parseInProgressStateIdResponse(response.data);
|
|
8341
|
+
if (!result.ok) {
|
|
8342
|
+
log.error(`[linear] team states GraphQL error: ${result.error}`);
|
|
8343
|
+
return result;
|
|
8344
|
+
}
|
|
8345
|
+
inProgressStateIdCache.set(teamId, result.data);
|
|
8346
|
+
return result;
|
|
8347
|
+
}
|
|
8348
|
+
function buildWebmuxAttachmentTitle(branch) {
|
|
8349
|
+
return `${WEBMUX_ATTACHMENT_TITLE_PREFIX}${branch}`;
|
|
8350
|
+
}
|
|
8351
|
+
function findWebmuxAttachment(issue, branch) {
|
|
8352
|
+
const candidates = issue.attachments.filter((a) => a.title.startsWith(WEBMUX_ATTACHMENT_TITLE_PREFIX));
|
|
8353
|
+
if (candidates.length === 0)
|
|
8354
|
+
return null;
|
|
8355
|
+
if (branch) {
|
|
8356
|
+
const exact = candidates.find((a) => a.title === buildWebmuxAttachmentTitle(branch));
|
|
8357
|
+
if (exact)
|
|
8358
|
+
return exact;
|
|
8359
|
+
}
|
|
8360
|
+
return [...candidates].sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] ?? null;
|
|
8361
|
+
}
|
|
8362
|
+
function inferPrStateFromAttachment(attachment) {
|
|
8363
|
+
const meta = attachment.metadata ?? {};
|
|
8364
|
+
const rawState = typeof meta.state === "string" ? meta.state.toLowerCase() : null;
|
|
8365
|
+
if (rawState === "open" || rawState === "closed" || rawState === "merged")
|
|
8366
|
+
return rawState;
|
|
8367
|
+
const status2 = typeof meta.status === "string" ? meta.status.toLowerCase() : null;
|
|
8368
|
+
if (status2 === "open" || status2 === "closed" || status2 === "merged")
|
|
8369
|
+
return status2;
|
|
8370
|
+
return "unknown";
|
|
8371
|
+
}
|
|
8372
|
+
function inferPrBranchFromAttachment(attachment) {
|
|
8373
|
+
const meta = attachment.metadata ?? {};
|
|
8374
|
+
if (typeof meta.branchName === "string" && meta.branchName.trim())
|
|
8375
|
+
return meta.branchName.trim();
|
|
8376
|
+
if (typeof meta.headRefName === "string" && meta.headRefName.trim())
|
|
8377
|
+
return meta.headRefName.trim();
|
|
8378
|
+
return null;
|
|
8379
|
+
}
|
|
8380
|
+
function findLinkedGitHubPr(issue) {
|
|
8381
|
+
const githubAttachments = issue.attachments.filter((a) => {
|
|
8382
|
+
if (a.sourceType === "github" || a.sourceType === "githubPR" || a.sourceType === "github_pull_request") {
|
|
8383
|
+
return true;
|
|
8384
|
+
}
|
|
8385
|
+
return /github\.com\/.+\/pull\/\d+/i.test(a.url);
|
|
8386
|
+
});
|
|
8387
|
+
if (githubAttachments.length === 0)
|
|
8388
|
+
return null;
|
|
8389
|
+
const prs = githubAttachments.map((a) => ({
|
|
8390
|
+
url: a.url,
|
|
8391
|
+
branch: inferPrBranchFromAttachment(a),
|
|
8392
|
+
state: inferPrStateFromAttachment(a)
|
|
8393
|
+
}));
|
|
8394
|
+
const indexed = prs.map((pr, idx) => ({ pr, idx, attachment: githubAttachments[idx] }));
|
|
8395
|
+
indexed.sort((a, b) => {
|
|
8396
|
+
const stateDiff = STATE_PRIORITY[a.pr.state] - STATE_PRIORITY[b.pr.state];
|
|
8397
|
+
if (stateDiff !== 0)
|
|
8398
|
+
return stateDiff;
|
|
8399
|
+
return b.attachment.createdAt.localeCompare(a.attachment.createdAt);
|
|
8400
|
+
});
|
|
8401
|
+
return indexed[0].pr;
|
|
8402
|
+
}
|
|
8403
|
+
async function fetchIssueWithAttachments(issueIdentifierOrId) {
|
|
8404
|
+
const response = await postLinearGraphql(ISSUE_WITH_ATTACHMENTS_QUERY, {
|
|
8405
|
+
id: issueIdentifierOrId
|
|
8406
|
+
});
|
|
8407
|
+
if (!response.ok) {
|
|
8408
|
+
return { ok: false, error: response.error, status: 502 };
|
|
8409
|
+
}
|
|
8410
|
+
const error = gqlErrorMessage(response.data);
|
|
8411
|
+
if (error) {
|
|
8412
|
+
return { ok: false, error, status: 502 };
|
|
8413
|
+
}
|
|
8414
|
+
const issue = response.data.data?.issue;
|
|
8415
|
+
if (!issue) {
|
|
8416
|
+
return { ok: false, error: `Linear issue not found: ${issueIdentifierOrId}`, status: 404 };
|
|
8417
|
+
}
|
|
8418
|
+
return {
|
|
8419
|
+
ok: true,
|
|
8420
|
+
data: {
|
|
8421
|
+
id: issue.id,
|
|
8422
|
+
identifier: issue.identifier,
|
|
8423
|
+
title: issue.title,
|
|
8424
|
+
description: issue.description,
|
|
8425
|
+
url: issue.url,
|
|
8426
|
+
branchName: issue.branchName,
|
|
8427
|
+
attachments: issue.attachments.nodes.map((node) => ({
|
|
8428
|
+
id: node.id,
|
|
8429
|
+
url: node.url,
|
|
8430
|
+
title: node.title,
|
|
8431
|
+
subtitle: node.subtitle,
|
|
8432
|
+
sourceType: node.sourceType,
|
|
8433
|
+
metadata: node.metadata,
|
|
8434
|
+
createdAt: node.createdAt
|
|
8435
|
+
}))
|
|
8436
|
+
}
|
|
8437
|
+
};
|
|
8438
|
+
}
|
|
8439
|
+
async function fetchTeamByKey(teamKey) {
|
|
8440
|
+
const response = await postLinearGraphql(TEAM_BY_KEY_QUERY, { key: teamKey });
|
|
8441
|
+
if (!response.ok) {
|
|
8442
|
+
return { ok: false, error: response.error, status: 502 };
|
|
8443
|
+
}
|
|
8444
|
+
const error = gqlErrorMessage(response.data);
|
|
8445
|
+
if (error) {
|
|
8446
|
+
return { ok: false, error, status: 502 };
|
|
8447
|
+
}
|
|
8448
|
+
const team = response.data.data?.teams.nodes[0];
|
|
8449
|
+
if (!team) {
|
|
8450
|
+
return { ok: false, error: `Linear team not found for key: ${teamKey}`, status: 404 };
|
|
8451
|
+
}
|
|
8452
|
+
return { ok: true, data: team };
|
|
8453
|
+
}
|
|
8454
|
+
async function createLinearIssue(input) {
|
|
8455
|
+
const viewerResult = await fetchViewerId();
|
|
8456
|
+
if (!viewerResult.ok) {
|
|
8457
|
+
return { ok: false, error: viewerResult.error };
|
|
8458
|
+
}
|
|
8459
|
+
const stateResult = await fetchInProgressStateId(input.teamId);
|
|
8460
|
+
if (!stateResult.ok) {
|
|
8461
|
+
return { ok: false, error: stateResult.error };
|
|
8462
|
+
}
|
|
8463
|
+
const response = await postLinearGraphql(ISSUE_CREATE_MUTATION, {
|
|
8464
|
+
input: {
|
|
8465
|
+
title: input.title,
|
|
8466
|
+
description: input.description,
|
|
8467
|
+
teamId: input.teamId,
|
|
8468
|
+
assigneeId: viewerResult.data,
|
|
8469
|
+
stateId: stateResult.data
|
|
8470
|
+
}
|
|
8471
|
+
});
|
|
8472
|
+
if (!response.ok) {
|
|
8473
|
+
log.error(`[linear] create failed: ${response.error}`);
|
|
8474
|
+
return { ok: false, error: response.error };
|
|
8475
|
+
}
|
|
8476
|
+
const result = parseIssueCreateResponse(response.data);
|
|
8477
|
+
if (result.ok) {
|
|
8478
|
+
issueCache = null;
|
|
8479
|
+
log.debug(`[linear] created issue ${result.data.identifier} branch=${result.data.branchName}`);
|
|
8480
|
+
} else {
|
|
8481
|
+
log.error(`[linear] issueCreate error: ${result.error}`);
|
|
8482
|
+
}
|
|
8483
|
+
return result;
|
|
8484
|
+
}
|
|
8485
|
+
var VIEWER_QUERY = `
|
|
8486
|
+
query Viewer {
|
|
8487
|
+
viewer {
|
|
8488
|
+
id
|
|
8489
|
+
}
|
|
8490
|
+
}
|
|
8491
|
+
`, TEAM_STATES_QUERY = `
|
|
8492
|
+
query TeamStates($teamId: String!) {
|
|
8493
|
+
team(id: $teamId) {
|
|
8494
|
+
states {
|
|
8495
|
+
nodes {
|
|
8496
|
+
id
|
|
8497
|
+
name
|
|
8498
|
+
type
|
|
8499
|
+
}
|
|
8500
|
+
}
|
|
8501
|
+
}
|
|
8502
|
+
}
|
|
8503
|
+
`, ISSUE_CREATE_MUTATION = `
|
|
8504
|
+
mutation IssueCreate($input: IssueCreateInput!) {
|
|
8505
|
+
issueCreate(input: $input) {
|
|
8506
|
+
success
|
|
8507
|
+
issue {
|
|
8508
|
+
id
|
|
8509
|
+
identifier
|
|
8510
|
+
title
|
|
8511
|
+
url
|
|
8512
|
+
branchName
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
`, issueCache = null, viewerIdCache = null, inProgressStateIdCache, ISSUE_WITH_ATTACHMENTS_QUERY = `
|
|
8517
|
+
query IssueWithAttachments($id: String!) {
|
|
8518
|
+
issue(id: $id) {
|
|
8519
|
+
id
|
|
8520
|
+
identifier
|
|
8521
|
+
title
|
|
8522
|
+
description
|
|
8523
|
+
url
|
|
8524
|
+
branchName
|
|
8525
|
+
attachments {
|
|
8526
|
+
nodes {
|
|
8527
|
+
id
|
|
8528
|
+
url
|
|
8529
|
+
title
|
|
8530
|
+
subtitle
|
|
8531
|
+
sourceType
|
|
8532
|
+
metadata
|
|
8533
|
+
createdAt
|
|
8534
|
+
}
|
|
8535
|
+
}
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
`, TEAM_BY_KEY_QUERY = `
|
|
8539
|
+
query TeamByKey($key: String!) {
|
|
8540
|
+
teams(filter: { key: { eq: $key } }, first: 1) {
|
|
8541
|
+
nodes {
|
|
8542
|
+
id
|
|
8543
|
+
key
|
|
8544
|
+
name
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
}
|
|
8548
|
+
`, WEBMUX_ATTACHMENT_TITLE_PREFIX = "webmux-state:", STATE_PRIORITY;
|
|
8549
|
+
var init_linear_service = __esm(() => {
|
|
8550
|
+
init_log();
|
|
8551
|
+
init_src();
|
|
8552
|
+
inProgressStateIdCache = new Map;
|
|
8553
|
+
STATE_PRIORITY = {
|
|
8554
|
+
open: 0,
|
|
8555
|
+
merged: 1,
|
|
8556
|
+
closed: 2,
|
|
8557
|
+
unknown: 3
|
|
8558
|
+
};
|
|
8559
|
+
});
|
|
8560
|
+
|
|
8561
|
+
// backend/src/services/conversation-export-service.ts
|
|
8562
|
+
function escapeFence(text) {
|
|
8563
|
+
return text.replace(/```/g, "``\u200B`");
|
|
8564
|
+
}
|
|
8565
|
+
function buildIssueHeader(issue) {
|
|
8566
|
+
const lines = [];
|
|
8567
|
+
lines.push(`This worktree is for Linear issue **${issue.identifier}** \u2014 ${issue.url}`);
|
|
8568
|
+
lines.push("");
|
|
8569
|
+
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}\`).`);
|
|
8570
|
+
lines.push("");
|
|
8571
|
+
lines.push(`## Issue: ${issue.title}`);
|
|
8572
|
+
if (issue.description?.trim()) {
|
|
8573
|
+
lines.push("");
|
|
8574
|
+
lines.push(escapeFence(issue.description.trim()));
|
|
8575
|
+
}
|
|
8576
|
+
lines.push("");
|
|
8577
|
+
return lines.join(`
|
|
8578
|
+
`);
|
|
8579
|
+
}
|
|
8580
|
+
function buildPriorConversationSection(payload) {
|
|
8581
|
+
const lines = [];
|
|
8582
|
+
lines.push(`---`);
|
|
8583
|
+
lines.push("");
|
|
8584
|
+
lines.push(`A previous webmux session for this issue was saved here (branch \`${payload.branch}\`${payload.baseBranch ? `, base \`${payload.baseBranch}\`` : ""}).`);
|
|
8585
|
+
lines.push("");
|
|
8586
|
+
lines.push("Previous conversation (chronological):");
|
|
8587
|
+
lines.push("");
|
|
8588
|
+
for (const message of payload.conversation) {
|
|
8589
|
+
lines.push(`### ${message.role}`);
|
|
8590
|
+
lines.push("");
|
|
8591
|
+
lines.push(escapeFence(message.text));
|
|
8592
|
+
lines.push("");
|
|
8593
|
+
}
|
|
8594
|
+
return lines.join(`
|
|
8595
|
+
`);
|
|
8596
|
+
}
|
|
8597
|
+
async function buildSeedFromLinear(input, deps2) {
|
|
8598
|
+
const issue = await deps2.fetchIssueWithAttachments(input.issueId);
|
|
8599
|
+
if (!issue.ok)
|
|
8600
|
+
return issue;
|
|
8601
|
+
const issueHeader = buildIssueHeader(issue.data);
|
|
8602
|
+
const webmuxAttachment = findWebmuxAttachment(issue.data, input.preferBranch);
|
|
8603
|
+
const pr = findLinkedGitHubPr(issue.data);
|
|
8604
|
+
let attachmentPayload = null;
|
|
8605
|
+
if (webmuxAttachment) {
|
|
8606
|
+
const payloadResult = await deps2.downloadWebmuxAttachment(webmuxAttachment.url);
|
|
8607
|
+
if (payloadResult.ok) {
|
|
8608
|
+
attachmentPayload = payloadResult.data;
|
|
8609
|
+
} else {
|
|
8610
|
+
log.error(`[linear] webmux attachment download failed: ${payloadResult.error}`);
|
|
8611
|
+
}
|
|
8612
|
+
}
|
|
8613
|
+
const source = attachmentPayload ? "webmux-attachment" : pr ? "github-integration" : "none";
|
|
8614
|
+
const branch = attachmentPayload?.branch ?? pr?.branch ?? (issue.data.branchName || null);
|
|
8615
|
+
const baseBranch = attachmentPayload?.baseBranch ?? null;
|
|
8616
|
+
const conversationMarkdown = attachmentPayload ? `${issueHeader}${buildPriorConversationSection(attachmentPayload)}` : issueHeader;
|
|
8617
|
+
return {
|
|
8618
|
+
ok: true,
|
|
8619
|
+
data: {
|
|
8620
|
+
source,
|
|
8621
|
+
branch,
|
|
8622
|
+
baseBranch,
|
|
8623
|
+
prUrl: pr?.url ?? null,
|
|
8624
|
+
conversationMarkdown
|
|
8625
|
+
}
|
|
8626
|
+
};
|
|
8627
|
+
}
|
|
8628
|
+
async function downloadWebmuxAttachmentDefault(url) {
|
|
8629
|
+
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
8630
|
+
if (!apiKey)
|
|
8631
|
+
return { ok: false, error: "LINEAR_API_KEY not set" };
|
|
8632
|
+
try {
|
|
8633
|
+
const res = await fetch(url, {
|
|
8634
|
+
headers: { Authorization: apiKey }
|
|
8635
|
+
});
|
|
8636
|
+
if (!res.ok) {
|
|
8637
|
+
return { ok: false, error: `Asset download failed ${res.status}` };
|
|
8638
|
+
}
|
|
8639
|
+
const text = await res.text();
|
|
8640
|
+
const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(JSON.parse(text));
|
|
8641
|
+
if (!parsed.success) {
|
|
8642
|
+
return { ok: false, error: "Asset is not a webmux conversation payload" };
|
|
8643
|
+
}
|
|
8644
|
+
return { ok: true, data: parsed.data };
|
|
8645
|
+
} catch (err) {
|
|
8646
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8647
|
+
return { ok: false, error: msg };
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
8650
|
+
var WebmuxConversationAttachmentPayloadSchema, defaultSeedFromLinearDeps;
|
|
8651
|
+
var init_conversation_export_service = __esm(() => {
|
|
8652
|
+
init_src();
|
|
8653
|
+
init_zod();
|
|
8654
|
+
init_log();
|
|
8655
|
+
init_linear_service();
|
|
8656
|
+
WebmuxConversationAttachmentPayloadSchema = exports_external.object({
|
|
8657
|
+
webmux: exports_external.literal(1),
|
|
8658
|
+
branch: exports_external.string(),
|
|
8659
|
+
baseBranch: exports_external.string().nullable(),
|
|
8660
|
+
agent: AgentIdSchema.nullable(),
|
|
8661
|
+
createdAt: exports_external.string(),
|
|
8662
|
+
conversation: exports_external.array(AgentsUiConversationMessageSchema)
|
|
8663
|
+
});
|
|
8664
|
+
defaultSeedFromLinearDeps = {
|
|
8665
|
+
fetchIssueWithAttachments,
|
|
8666
|
+
downloadWebmuxAttachment: downloadWebmuxAttachmentDefault
|
|
8667
|
+
};
|
|
8668
|
+
});
|
|
8669
|
+
|
|
8670
|
+
// bin/src/oneshot.ts
|
|
8671
|
+
var exports_oneshot = {};
|
|
8672
|
+
__export(exports_oneshot, {
|
|
8673
|
+
runOneshotCommand: () => runOneshotCommand,
|
|
8674
|
+
runOneshot: () => runOneshot,
|
|
8675
|
+
parseOneshotArgs: () => parseOneshotArgs,
|
|
8676
|
+
getOneshotUsage: () => getOneshotUsage
|
|
8677
|
+
});
|
|
8678
|
+
function getOneshotUsage() {
|
|
8679
|
+
return [
|
|
8680
|
+
"Usage:",
|
|
8681
|
+
" webmux oneshot [branch] --prompt <text> [--agent <id>] [--base <branch>] [--profile <name>]",
|
|
8682
|
+
" [--env KEY=VALUE]... [--keep-open] [--linear <issue-id|team-key>]",
|
|
8683
|
+
" webmux oneshot --resume <branch> --prompt <text>",
|
|
8684
|
+
"",
|
|
8685
|
+
"Runs an agent worktree start-to-finish, streaming the conversation to stdout.",
|
|
8686
|
+
"Does not change the focused tmux session. The server-side oneshot watcher",
|
|
8687
|
+
"closes the worktree session (and posts the conversation back to Linear, if",
|
|
8688
|
+
"--linear is set) once the agent finishes \u2014 even if this CLI is killed mid-run.",
|
|
8689
|
+
"Opening the worktree in the browser and interacting with it disarms the watcher.",
|
|
8690
|
+
"",
|
|
8691
|
+
"Exit codes: 0 if the agent opened a PR / the user took over via the browser;",
|
|
8692
|
+
"1 if the agent went idle without opening a PR; 130 on Ctrl-C (worktree keeps",
|
|
8693
|
+
"running, resume with `webmux oneshot --resume <branch>`).",
|
|
8694
|
+
"",
|
|
8695
|
+
"Options:",
|
|
8696
|
+
" --resume <branch> Resume an existing local worktree instead of creating one",
|
|
8697
|
+
" --prompt <text> Initial agent prompt (required; follow-up nudge when --resume)",
|
|
8698
|
+
" --agent <id> Agent id to launch",
|
|
8699
|
+
" --base <branch> Base branch for a new worktree (defaults to config)",
|
|
8700
|
+
" --profile <name> Worktree profile from .webmux.yaml",
|
|
8701
|
+
" --env KEY=VALUE Runtime env override (repeatable)",
|
|
8702
|
+
" --keep-open Don't auto-close the worktree session when the agent finishes",
|
|
8703
|
+
" --linear ID|TEAM Tie this oneshot to Linear:",
|
|
8704
|
+
" ENG-123 \u2014 load the issue body as context, post results back",
|
|
8705
|
+
" ENG \u2014 create a new issue in that team when done",
|
|
8706
|
+
" --branch <name> Override the branch when --linear resolves to one",
|
|
8707
|
+
" --help Show this help message"
|
|
8708
|
+
].join(`
|
|
8709
|
+
`);
|
|
8710
|
+
}
|
|
8711
|
+
function readOptionValue(args, index, flag) {
|
|
8712
|
+
const arg = args[index];
|
|
8713
|
+
if (!arg)
|
|
8714
|
+
throw new CommandUsageError(`${flag} requires a value`);
|
|
8715
|
+
const prefix = `${flag}=`;
|
|
8716
|
+
if (arg.startsWith(prefix))
|
|
8717
|
+
return { value: arg.slice(prefix.length), nextIndex: index };
|
|
8718
|
+
const value = args[index + 1];
|
|
8719
|
+
if (value === undefined)
|
|
8720
|
+
throw new CommandUsageError(`${flag} requires a value`);
|
|
8721
|
+
return { value, nextIndex: index + 1 };
|
|
8722
|
+
}
|
|
8723
|
+
function parseOneshotArgs(args) {
|
|
8724
|
+
const body = {};
|
|
8725
|
+
const envOverrides = {};
|
|
8726
|
+
let branch = null;
|
|
8727
|
+
let branchFlagUsed = false;
|
|
8728
|
+
let prompt = null;
|
|
8729
|
+
let resume = false;
|
|
8730
|
+
let resumeBranch = null;
|
|
8731
|
+
let keepOpen = false;
|
|
8732
|
+
let fromLinearIssueId = null;
|
|
8733
|
+
let postToLinearTarget = null;
|
|
8734
|
+
for (let index = 0;index < args.length; index++) {
|
|
8735
|
+
const arg = args[index];
|
|
8736
|
+
if (!arg)
|
|
8737
|
+
continue;
|
|
8738
|
+
if (arg === "--help" || arg === "-h")
|
|
8739
|
+
return null;
|
|
8740
|
+
if (arg === "--resume" || arg.startsWith("--resume=")) {
|
|
8741
|
+
const { value, nextIndex } = readOptionValue(args, index, "--resume");
|
|
8742
|
+
resume = true;
|
|
8743
|
+
resumeBranch = value.trim();
|
|
8744
|
+
index = nextIndex;
|
|
8745
|
+
continue;
|
|
8746
|
+
}
|
|
8747
|
+
if (arg === "--prompt" || arg.startsWith("--prompt=")) {
|
|
8748
|
+
const { value, nextIndex } = readOptionValue(args, index, "--prompt");
|
|
8749
|
+
prompt = value;
|
|
8750
|
+
index = nextIndex;
|
|
8751
|
+
continue;
|
|
8752
|
+
}
|
|
8753
|
+
if (arg === "--agent" || arg.startsWith("--agent=")) {
|
|
8754
|
+
const { value, nextIndex } = readOptionValue(args, index, "--agent");
|
|
8755
|
+
body.agent = value.trim();
|
|
8756
|
+
index = nextIndex;
|
|
8757
|
+
continue;
|
|
8758
|
+
}
|
|
8759
|
+
if (arg === "--base" || arg.startsWith("--base=")) {
|
|
8760
|
+
const { value, nextIndex } = readOptionValue(args, index, "--base");
|
|
8761
|
+
body.baseBranch = value;
|
|
8762
|
+
index = nextIndex;
|
|
8763
|
+
continue;
|
|
8764
|
+
}
|
|
8765
|
+
if (arg === "--profile" || arg.startsWith("--profile=")) {
|
|
8766
|
+
const { value, nextIndex } = readOptionValue(args, index, "--profile");
|
|
8767
|
+
body.profile = value;
|
|
8768
|
+
index = nextIndex;
|
|
8769
|
+
continue;
|
|
8770
|
+
}
|
|
8771
|
+
if (arg === "--env" || arg.startsWith("--env=")) {
|
|
8772
|
+
const { value, nextIndex } = readOptionValue(args, index, "--env");
|
|
8773
|
+
const sep = value.indexOf("=");
|
|
8774
|
+
if (sep <= 0)
|
|
8775
|
+
throw new CommandUsageError("--env must use KEY=VALUE");
|
|
8776
|
+
envOverrides[value.slice(0, sep)] = value.slice(sep + 1);
|
|
8777
|
+
index = nextIndex;
|
|
8778
|
+
continue;
|
|
8779
|
+
}
|
|
8780
|
+
if (arg === "--keep-open") {
|
|
8781
|
+
keepOpen = true;
|
|
8782
|
+
continue;
|
|
8783
|
+
}
|
|
8784
|
+
if (arg === "--linear" || arg.startsWith("--linear=")) {
|
|
8785
|
+
const { value, nextIndex } = readOptionValue(args, index, "--linear");
|
|
8786
|
+
const target = parseLinearTarget(value);
|
|
8787
|
+
if (target.kind === "issue") {
|
|
8788
|
+
fromLinearIssueId = target.issueId;
|
|
8789
|
+
postToLinearTarget = { kind: "issue", issueId: target.issueId };
|
|
8790
|
+
} else if (target.kind === "team") {
|
|
8791
|
+
postToLinearTarget = { kind: "team", teamKey: target.teamKey };
|
|
8792
|
+
} else {
|
|
8793
|
+
throw new CommandUsageError(`--linear expects either an issue id (ENG-123) or a team key (ENG); got "${target.raw}"`);
|
|
8794
|
+
}
|
|
8795
|
+
index = nextIndex;
|
|
8796
|
+
continue;
|
|
8797
|
+
}
|
|
8798
|
+
if (arg === "--branch" || arg.startsWith("--branch=")) {
|
|
8799
|
+
const { value, nextIndex } = readOptionValue(args, index, "--branch");
|
|
8800
|
+
if (branch && branch !== value) {
|
|
8801
|
+
throw new CommandUsageError(`Conflicting branch values: "${branch}" and "${value}"`);
|
|
8802
|
+
}
|
|
8803
|
+
branch = value.trim();
|
|
8804
|
+
branchFlagUsed = true;
|
|
8805
|
+
index = nextIndex;
|
|
8806
|
+
continue;
|
|
8807
|
+
}
|
|
8808
|
+
if (arg.startsWith("-")) {
|
|
8809
|
+
throw new CommandUsageError(`Unknown option: ${arg}`);
|
|
8810
|
+
}
|
|
8811
|
+
if (branch) {
|
|
8812
|
+
throw new CommandUsageError(`Unexpected argument: ${arg}`);
|
|
8813
|
+
}
|
|
8814
|
+
branch = arg;
|
|
8815
|
+
}
|
|
8816
|
+
if (resume) {
|
|
8817
|
+
if (fromLinearIssueId) {
|
|
8818
|
+
throw new CommandUsageError("Cannot use --resume with --linear <issue-id>");
|
|
8819
|
+
}
|
|
8820
|
+
if (branchFlagUsed) {
|
|
8821
|
+
throw new CommandUsageError("Cannot use --branch with --resume; --resume already names the branch");
|
|
8822
|
+
}
|
|
8823
|
+
if (!resumeBranch)
|
|
8824
|
+
throw new CommandUsageError("--resume requires a branch name");
|
|
8825
|
+
if (branch && branch !== resumeBranch) {
|
|
8826
|
+
throw new CommandUsageError("Cannot pass both a positional branch and --resume");
|
|
8827
|
+
}
|
|
8828
|
+
if (!prompt) {
|
|
8829
|
+
throw new CommandUsageError("--resume requires --prompt; use the dashboard to re-attach without re-prompting");
|
|
8830
|
+
}
|
|
8831
|
+
branch = resumeBranch;
|
|
8832
|
+
}
|
|
8833
|
+
if (branchFlagUsed && !fromLinearIssueId) {
|
|
8834
|
+
throw new CommandUsageError("--branch only applies with --linear; pass the branch as a positional argument otherwise");
|
|
8835
|
+
}
|
|
8836
|
+
if (!resume && !fromLinearIssueId && !prompt) {
|
|
8837
|
+
throw new CommandUsageError("oneshot requires --prompt (or use --linear)");
|
|
8838
|
+
}
|
|
8839
|
+
if (branch)
|
|
8840
|
+
body.branch = branch;
|
|
8841
|
+
if (prompt)
|
|
8842
|
+
body.prompt = prompt;
|
|
8843
|
+
if (Object.keys(envOverrides).length > 0)
|
|
8844
|
+
body.envOverrides = envOverrides;
|
|
8845
|
+
return {
|
|
8846
|
+
branch,
|
|
8847
|
+
prompt,
|
|
8848
|
+
resume,
|
|
8849
|
+
body,
|
|
8850
|
+
keepOpen,
|
|
8851
|
+
fromLinearIssueId,
|
|
8852
|
+
postToLinearTarget
|
|
8853
|
+
};
|
|
8854
|
+
}
|
|
8855
|
+
function timestamp() {
|
|
8856
|
+
const d = new Date;
|
|
8857
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
8858
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
8859
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
8860
|
+
return `${hh}:${mm}:${ss}`;
|
|
8861
|
+
}
|
|
8862
|
+
function formatLogLine(role, text) {
|
|
8863
|
+
return `[${timestamp()}] [${role}] ${text}`;
|
|
8864
|
+
}
|
|
8865
|
+
function truncateInline(text, limit) {
|
|
8866
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
8867
|
+
if (collapsed.length <= limit)
|
|
8868
|
+
return collapsed;
|
|
8869
|
+
return `${collapsed.slice(0, limit)}\u2026`;
|
|
8870
|
+
}
|
|
8871
|
+
function summarizeToolInput(toolName, jsonText) {
|
|
8872
|
+
let input = null;
|
|
8873
|
+
try {
|
|
8874
|
+
const parsed = JSON.parse(jsonText);
|
|
8875
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
8876
|
+
input = parsed;
|
|
8877
|
+
}
|
|
8878
|
+
} catch {
|
|
8879
|
+
return truncateInline(jsonText, 100);
|
|
8880
|
+
}
|
|
8881
|
+
if (!input)
|
|
8882
|
+
return truncateInline(jsonText, 100);
|
|
8883
|
+
const keys = TOOL_PRIMARY_KEY[toolName.toLowerCase()];
|
|
8884
|
+
if (keys) {
|
|
8885
|
+
const values = [];
|
|
8886
|
+
for (const key of keys) {
|
|
8887
|
+
const v2 = input[key];
|
|
8888
|
+
if (typeof v2 === "string" && v2.length > 0)
|
|
8889
|
+
values.push(v2);
|
|
8890
|
+
}
|
|
8891
|
+
if (values.length > 0)
|
|
8892
|
+
return truncateInline(values.join(" "), 120);
|
|
8893
|
+
}
|
|
8894
|
+
const parts = [];
|
|
8895
|
+
for (const [key, value] of Object.entries(input)) {
|
|
8896
|
+
if (typeof value === "string")
|
|
8897
|
+
parts.push(`${key}=${truncateInline(value, 40)}`);
|
|
8898
|
+
}
|
|
8899
|
+
if (parts.length === 0)
|
|
8900
|
+
return "";
|
|
8901
|
+
return truncateInline(parts.join(" "), 120);
|
|
8902
|
+
}
|
|
8903
|
+
function summarizeToolResult(text) {
|
|
8904
|
+
const lines = text.split(`
|
|
8905
|
+
`).map((l) => l.trimEnd()).filter((l) => l.length > 0);
|
|
8906
|
+
if (lines.length === 0)
|
|
8907
|
+
return "(empty)";
|
|
8908
|
+
const first = truncateInline(lines[0], 200);
|
|
8909
|
+
return lines.length > 1 ? `${first} (+${lines.length - 1} lines)` : first;
|
|
8910
|
+
}
|
|
8911
|
+
function formatConversationLine(message) {
|
|
8912
|
+
const kind = message.kind ?? "text";
|
|
8913
|
+
if (kind === "toolUse") {
|
|
8914
|
+
const tool = message.toolName ?? "tool";
|
|
8915
|
+
const summary = summarizeToolInput(tool, message.text);
|
|
8916
|
+
return `[${timestamp()}] \u25CF ${tool}(${summary})`;
|
|
8917
|
+
}
|
|
8918
|
+
if (kind === "toolResult") {
|
|
8919
|
+
return `[${timestamp()}] \u23BF ${summarizeToolResult(message.text)}`;
|
|
8920
|
+
}
|
|
8921
|
+
return formatLogLine(message.role, message.text);
|
|
8922
|
+
}
|
|
8923
|
+
function flushStreamingLine(state) {
|
|
8924
|
+
if (state.streamingItemId !== null) {
|
|
8925
|
+
process.stdout.write(`
|
|
8926
|
+
`);
|
|
8927
|
+
state.streamingItemId = null;
|
|
8928
|
+
state.streamingNeedsHeader = false;
|
|
8929
|
+
}
|
|
8930
|
+
}
|
|
8931
|
+
function printNewMessages(state, messages) {
|
|
8932
|
+
for (const message of messages) {
|
|
8933
|
+
if (state.printedMessageIds.has(message.id))
|
|
8934
|
+
continue;
|
|
8935
|
+
if (state.streamingItemId === message.id) {
|
|
8936
|
+
state.printedMessageIds.add(message.id);
|
|
8937
|
+
if (message.status === "completed")
|
|
8938
|
+
flushStreamingLine(state);
|
|
8939
|
+
continue;
|
|
8940
|
+
}
|
|
8941
|
+
flushStreamingLine(state);
|
|
8942
|
+
if (message.text.trim().length === 0) {
|
|
8943
|
+
state.printedMessageIds.add(message.id);
|
|
8944
|
+
continue;
|
|
8945
|
+
}
|
|
8946
|
+
process.stdout.write(`${formatConversationLine(message)}
|
|
8947
|
+
`);
|
|
8948
|
+
state.printedMessageIds.add(message.id);
|
|
8949
|
+
}
|
|
8950
|
+
}
|
|
8951
|
+
function handleConversationEvent(event, state, stderr) {
|
|
8952
|
+
if (event.type === "snapshot") {
|
|
8953
|
+
printNewMessages(state, event.data.conversation.messages);
|
|
8954
|
+
return;
|
|
8955
|
+
}
|
|
8956
|
+
if (event.type === "messageDelta") {
|
|
8957
|
+
if (state.streamingItemId !== event.itemId) {
|
|
8958
|
+
flushStreamingLine(state);
|
|
8959
|
+
state.streamingItemId = event.itemId;
|
|
8960
|
+
state.streamingNeedsHeader = true;
|
|
8961
|
+
}
|
|
8962
|
+
if (state.streamingNeedsHeader) {
|
|
8963
|
+
process.stdout.write(`[${timestamp()}] [assistant] `);
|
|
8964
|
+
state.streamingNeedsHeader = false;
|
|
8965
|
+
}
|
|
8966
|
+
process.stdout.write(event.delta);
|
|
8967
|
+
return;
|
|
8968
|
+
}
|
|
8969
|
+
if (event.type === "error") {
|
|
8970
|
+
flushStreamingLine(state);
|
|
8971
|
+
stderr(`[${timestamp()}] [error] ${event.message}`);
|
|
8972
|
+
return;
|
|
8973
|
+
}
|
|
8974
|
+
}
|
|
8975
|
+
function streamConversation(branch, port, state, stderr, onFatal) {
|
|
8976
|
+
let closed = false;
|
|
8977
|
+
let socket = null;
|
|
8978
|
+
let reconnectTimer = null;
|
|
8979
|
+
let consecutiveFailures = 0;
|
|
8980
|
+
const connect = () => {
|
|
8981
|
+
if (closed)
|
|
8982
|
+
return;
|
|
8983
|
+
const url = `ws://localhost:${port}${apiPaths.streamAgentsWorktreeConversation.replace(":name", encodeURIComponent(branch))}`;
|
|
8984
|
+
const ws = new WebSocket(url);
|
|
8985
|
+
socket = ws;
|
|
8986
|
+
ws.addEventListener("open", () => {
|
|
8987
|
+
consecutiveFailures = 0;
|
|
8988
|
+
});
|
|
8989
|
+
ws.addEventListener("message", (event) => {
|
|
8990
|
+
if (typeof event.data !== "string")
|
|
8991
|
+
return;
|
|
8992
|
+
try {
|
|
8993
|
+
const parsed = AgentsUiConversationEventSchema.parse(JSON.parse(event.data));
|
|
8994
|
+
handleConversationEvent(parsed, state, stderr);
|
|
8995
|
+
} catch {
|
|
8996
|
+
stderr(`[${timestamp()}] [error] received malformed conversation stream data`);
|
|
8997
|
+
}
|
|
8998
|
+
});
|
|
8999
|
+
ws.addEventListener("close", () => {
|
|
9000
|
+
socket = null;
|
|
9001
|
+
if (closed)
|
|
9002
|
+
return;
|
|
9003
|
+
consecutiveFailures += 1;
|
|
9004
|
+
if (RECONNECT_WARN_AT.includes(consecutiveFailures)) {
|
|
9005
|
+
stderr(`[${timestamp()}] [warn] webmux server unreachable, retrying (${consecutiveFailures}/${MAX_CONSECUTIVE_RECONNECTS})`);
|
|
9006
|
+
}
|
|
9007
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_RECONNECTS) {
|
|
9008
|
+
closed = true;
|
|
9009
|
+
onFatal(`webmux server unreachable after ${consecutiveFailures} reconnect attempts`);
|
|
9010
|
+
return;
|
|
9011
|
+
}
|
|
9012
|
+
reconnectTimer = setTimeout(connect, 2000);
|
|
9013
|
+
});
|
|
9014
|
+
ws.addEventListener("error", () => {});
|
|
9015
|
+
};
|
|
9016
|
+
connect();
|
|
9017
|
+
return {
|
|
9018
|
+
close: () => {
|
|
9019
|
+
closed = true;
|
|
9020
|
+
if (reconnectTimer)
|
|
9021
|
+
clearTimeout(reconnectTimer);
|
|
9022
|
+
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
|
9023
|
+
socket.close();
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
9026
|
+
};
|
|
9027
|
+
}
|
|
9028
|
+
function recordPrEvents(state, worktree, onPrEvent) {
|
|
9029
|
+
for (const pr of worktree.prs) {
|
|
9030
|
+
if (!state.seenPrUrls.has(pr.url)) {
|
|
9031
|
+
state.seenPrUrls.add(pr.url);
|
|
9032
|
+
onPrEvent(`PR #${pr.number} opened: ${pr.url}`);
|
|
9033
|
+
}
|
|
9034
|
+
if (pr.state === "merged" && !state.seenMergedUrls.has(pr.url)) {
|
|
9035
|
+
state.seenMergedUrls.add(pr.url);
|
|
9036
|
+
onPrEvent(`PR #${pr.number} merged: ${pr.url}`);
|
|
9037
|
+
}
|
|
9038
|
+
}
|
|
9039
|
+
}
|
|
9040
|
+
function pollProjectState(branch, port, state, callbacks, stderr) {
|
|
9041
|
+
const api = createApi(`http://localhost:${port}`);
|
|
9042
|
+
let stopped = false;
|
|
9043
|
+
let timer = null;
|
|
9044
|
+
const forcePrSync = async () => {
|
|
9045
|
+
try {
|
|
9046
|
+
const refreshed = await api.syncWorktreePrs({ params: { name: branch } });
|
|
9047
|
+
recordPrEvents(state, refreshed, callbacks.onPrEvent);
|
|
9048
|
+
} catch (err) {
|
|
9049
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9050
|
+
stderr(`[${timestamp()}] [warn] failed to sync PRs from server: ${msg}`);
|
|
9051
|
+
}
|
|
9052
|
+
};
|
|
9053
|
+
const tick = async () => {
|
|
9054
|
+
if (stopped)
|
|
9055
|
+
return;
|
|
9056
|
+
try {
|
|
9057
|
+
const response = await api.fetchWorktrees();
|
|
9058
|
+
const worktree = response.worktrees.find((w3) => w3.branch === branch);
|
|
9059
|
+
if (!worktree) {
|
|
9060
|
+
if (state.hadOpenSession) {
|
|
9061
|
+
callbacks.onWorktreeRemoved();
|
|
9062
|
+
return;
|
|
9063
|
+
}
|
|
9064
|
+
} else {
|
|
9065
|
+
if (worktree.mux) {
|
|
9066
|
+
state.hadOpenSession = true;
|
|
9067
|
+
state.consecutiveClosedReadings = 0;
|
|
9068
|
+
}
|
|
9069
|
+
recordPrEvents(state, worktree, callbacks.onPrEvent);
|
|
9070
|
+
if (worktree.oneshot) {
|
|
9071
|
+
state.watcherWasArmed = true;
|
|
9072
|
+
} else if (state.watcherWasArmed && worktree.mux) {
|
|
9073
|
+
callbacks.onUserTookOver();
|
|
9074
|
+
return;
|
|
9075
|
+
}
|
|
9076
|
+
if (state.hadOpenSession && !worktree.mux) {
|
|
9077
|
+
state.consecutiveClosedReadings += 1;
|
|
9078
|
+
if (state.consecutiveClosedReadings >= 2) {
|
|
9079
|
+
callbacks.onSessionClosed();
|
|
9080
|
+
return;
|
|
9081
|
+
}
|
|
9082
|
+
}
|
|
9083
|
+
const status2 = worktree.status;
|
|
9084
|
+
const isTerminal = status2 === "stopped" || status2 === "error";
|
|
9085
|
+
const isIdle = status2 === "idle";
|
|
9086
|
+
if (isTerminal || isIdle) {
|
|
9087
|
+
if (state.idleSinceMs === null)
|
|
9088
|
+
state.idleSinceMs = Date.now();
|
|
9089
|
+
const isStable = isTerminal || Date.now() - state.idleSinceMs >= IDLE_GRACE_MS;
|
|
9090
|
+
if (isStable) {
|
|
9091
|
+
await forcePrSync();
|
|
9092
|
+
if (state.seenPrUrls.size > 0) {
|
|
9093
|
+
callbacks.onAgentDone(`agent ${status2} after opening PR`);
|
|
9094
|
+
} else {
|
|
9095
|
+
callbacks.onAgentStuck(`agent ${status2} without opening a PR`);
|
|
9096
|
+
}
|
|
9097
|
+
return;
|
|
9098
|
+
}
|
|
9099
|
+
} else {
|
|
9100
|
+
state.idleSinceMs = null;
|
|
9101
|
+
}
|
|
9102
|
+
}
|
|
9103
|
+
} catch {}
|
|
9104
|
+
timer = setTimeout(tick, 3000);
|
|
9105
|
+
};
|
|
9106
|
+
tick();
|
|
9107
|
+
return {
|
|
9108
|
+
stop: () => {
|
|
9109
|
+
stopped = true;
|
|
9110
|
+
if (timer)
|
|
9111
|
+
clearTimeout(timer);
|
|
9112
|
+
}
|
|
9113
|
+
};
|
|
9114
|
+
}
|
|
9115
|
+
async function ensureWorktreeReady(branch, port, stderr) {
|
|
9116
|
+
const api = createApi(`http://localhost:${port}`);
|
|
9117
|
+
const deadline = Date.now() + 60000;
|
|
9118
|
+
while (Date.now() < deadline) {
|
|
9119
|
+
try {
|
|
9120
|
+
const response = await api.fetchWorktrees();
|
|
9121
|
+
const worktree = response.worktrees.find((w3) => w3.branch === branch);
|
|
9122
|
+
if (worktree && worktree.mux && worktree.status !== "creating" && worktree.status !== "closed") {
|
|
9123
|
+
return { ready: true, worktree };
|
|
9124
|
+
}
|
|
9125
|
+
} catch {}
|
|
9126
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
9127
|
+
}
|
|
9128
|
+
stderr(`[${timestamp()}] [error] timed out waiting for ${branch} session to start`);
|
|
9129
|
+
return { ready: false };
|
|
9130
|
+
}
|
|
9131
|
+
function printConversationHistory(initial, state) {
|
|
9132
|
+
printNewMessages(state, initial.conversation.messages);
|
|
9133
|
+
}
|
|
9134
|
+
function pollConversationHistory(branch, port, state) {
|
|
9135
|
+
const api = createApi(`http://localhost:${port}`);
|
|
9136
|
+
let stopped = false;
|
|
9137
|
+
let timer = null;
|
|
9138
|
+
const tick = async () => {
|
|
9139
|
+
if (stopped)
|
|
9140
|
+
return;
|
|
9141
|
+
try {
|
|
9142
|
+
const response = await api.fetchAgentsWorktreeConversationHistory({ params: { name: branch } });
|
|
9143
|
+
printNewMessages(state, response.conversation.messages);
|
|
9144
|
+
} catch {}
|
|
9145
|
+
if (!stopped)
|
|
9146
|
+
timer = setTimeout(tick, 2000);
|
|
9147
|
+
};
|
|
9148
|
+
tick();
|
|
9149
|
+
return {
|
|
9150
|
+
stop: () => {
|
|
9151
|
+
stopped = true;
|
|
9152
|
+
if (timer)
|
|
9153
|
+
clearTimeout(timer);
|
|
9154
|
+
}
|
|
9155
|
+
};
|
|
9156
|
+
}
|
|
9157
|
+
function deriveOneshotIssueTitle(prompt) {
|
|
9158
|
+
if (!prompt)
|
|
9159
|
+
return null;
|
|
9160
|
+
const firstLine = prompt.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
|
|
9161
|
+
if (!firstLine)
|
|
9162
|
+
return null;
|
|
9163
|
+
return firstLine.length > 100 ? `${firstLine.slice(0, 97)}\u2026` : firstLine;
|
|
9164
|
+
}
|
|
9165
|
+
async function runOneshot(parsed, port) {
|
|
9166
|
+
const stdout = (line) => {
|
|
9167
|
+
process.stdout.write(`${line}
|
|
9168
|
+
`);
|
|
9169
|
+
};
|
|
9170
|
+
const stderr = (line) => {
|
|
9171
|
+
process.stderr.write(`${line}
|
|
9172
|
+
`);
|
|
9173
|
+
};
|
|
9174
|
+
const api = createApi(`http://localhost:${port}`);
|
|
9175
|
+
let branch = parsed.branch;
|
|
9176
|
+
const body = { ...parsed.body };
|
|
9177
|
+
let fromLinearIssueId = parsed.fromLinearIssueId;
|
|
9178
|
+
let postToLinearTarget = parsed.postToLinearTarget;
|
|
9179
|
+
try {
|
|
9180
|
+
if (postToLinearTarget) {
|
|
9181
|
+
const availability = await api.fetchLinearIssues();
|
|
9182
|
+
if (availability.availability === "missing_api_key") {
|
|
9183
|
+
stderr(`[${timestamp()}] [error] server has no LINEAR_API_KEY \u2014 the post-back to Linear at the end of the run will fail. Set the env var on the webmux server and restart it.`);
|
|
9184
|
+
return 1;
|
|
9185
|
+
}
|
|
9186
|
+
if (availability.availability === "disabled") {
|
|
9187
|
+
stderr(`[${timestamp()}] [error] Linear integration is disabled on the webmux server.`);
|
|
9188
|
+
return 1;
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
9191
|
+
if (postToLinearTarget?.kind === "team") {
|
|
9192
|
+
const title = deriveOneshotIssueTitle(parsed.prompt);
|
|
9193
|
+
if (!title) {
|
|
9194
|
+
stderr(`[${timestamp()}] [error] --linear ${postToLinearTarget.teamKey} requires --prompt to derive an issue title`);
|
|
9195
|
+
return 1;
|
|
9196
|
+
}
|
|
9197
|
+
if (parsed.resume) {
|
|
9198
|
+
stdout(`[${timestamp()}] [event] no Linear issue for this resume; creating a fresh ${postToLinearTarget.teamKey}-N for the post-back`);
|
|
9199
|
+
}
|
|
9200
|
+
stdout(`[${timestamp()}] [event] creating Linear issue in team ${postToLinearTarget.teamKey}...`);
|
|
9201
|
+
const team = await fetchTeamByKey(postToLinearTarget.teamKey);
|
|
9202
|
+
if (!team.ok) {
|
|
9203
|
+
stderr(`[${timestamp()}] [error] Linear team lookup failed: ${team.error}`);
|
|
9204
|
+
return 1;
|
|
9205
|
+
}
|
|
9206
|
+
const created = await createLinearIssue({
|
|
9207
|
+
teamId: team.data.id,
|
|
9208
|
+
title,
|
|
9209
|
+
description: ""
|
|
9210
|
+
});
|
|
9211
|
+
if (!created.ok) {
|
|
9212
|
+
stderr(`[${timestamp()}] [error] Linear issue creation failed: ${created.error}`);
|
|
9213
|
+
return 1;
|
|
9214
|
+
}
|
|
9215
|
+
stdout(`[${timestamp()}] [event] created Linear issue ${created.data.identifier} \u2192 ${created.data.url}`);
|
|
9216
|
+
fromLinearIssueId = created.data.identifier;
|
|
9217
|
+
postToLinearTarget = { kind: "issue", issueId: created.data.identifier };
|
|
9218
|
+
}
|
|
9219
|
+
if (fromLinearIssueId) {
|
|
9220
|
+
stdout(`[${timestamp()}] [event] resolving Linear issue ${fromLinearIssueId}...`);
|
|
9221
|
+
const seedResult = await buildSeedFromLinear({ issueId: fromLinearIssueId }, defaultSeedFromLinearDeps);
|
|
9222
|
+
if (!seedResult.ok) {
|
|
9223
|
+
stderr(`[${timestamp()}] [error] Linear seed lookup failed: ${seedResult.error}`);
|
|
9224
|
+
return 1;
|
|
9225
|
+
}
|
|
9226
|
+
const seed = seedResult.data;
|
|
9227
|
+
stdout(`[${timestamp()}] [event] seed source: ${seed.source}${seed.branch ? ` branch=${seed.branch}` : ""}${seed.prUrl ? ` pr=${seed.prUrl}` : ""}`);
|
|
9228
|
+
const resolvedBranch = branch ?? seed.branch ?? null;
|
|
9229
|
+
if (!resolvedBranch) {
|
|
9230
|
+
stderr(`[${timestamp()}] [error] Linear issue did not resolve to a branch; pass --branch to override.`);
|
|
9231
|
+
return 1;
|
|
9232
|
+
}
|
|
9233
|
+
branch = resolvedBranch;
|
|
9234
|
+
body.branch = resolvedBranch;
|
|
9235
|
+
if (seed.source !== "none")
|
|
9236
|
+
body.mode = "existing";
|
|
9237
|
+
body.fromLinear = {
|
|
9238
|
+
issueId: fromLinearIssueId,
|
|
9239
|
+
...seed.conversationMarkdown ? { conversationContext: seed.conversationMarkdown } : {}
|
|
9240
|
+
};
|
|
9241
|
+
}
|
|
9242
|
+
const existingWorktree = branch ? (await api.fetchWorktrees()).worktrees.find((w3) => w3.branch === branch) : undefined;
|
|
9243
|
+
const oneshotConfig = {
|
|
9244
|
+
autoCloseOnDone: !parsed.keepOpen,
|
|
9245
|
+
...postToLinearTarget ? { postToLinearOnDone: postToLinearTarget } : {}
|
|
9246
|
+
};
|
|
9247
|
+
if (parsed.resume || existingWorktree) {
|
|
9248
|
+
if (!branch)
|
|
9249
|
+
throw new Error("resume requires a branch name");
|
|
9250
|
+
const reason = parsed.resume ? "resuming" : `worktree exists, resuming ${branch}`;
|
|
9251
|
+
stdout(`[${timestamp()}] [event] ${reason}`);
|
|
9252
|
+
if (fromLinearIssueId) {
|
|
9253
|
+
stdout(`[${timestamp()}] [event] skipping Linear seed \u2014 agent's existing session history already covers it`);
|
|
9254
|
+
}
|
|
9255
|
+
await api.openWorktree({
|
|
9256
|
+
params: { name: branch },
|
|
9257
|
+
body: {
|
|
9258
|
+
...parsed.prompt ? { prompt: parsed.prompt } : {},
|
|
9259
|
+
oneshot: oneshotConfig
|
|
9260
|
+
}
|
|
9261
|
+
});
|
|
9262
|
+
if (parsed.prompt)
|
|
9263
|
+
stdout(`[${timestamp()}] [event] sent prompt`);
|
|
9264
|
+
} else {
|
|
9265
|
+
stdout(`[${timestamp()}] [event] creating worktree${branch ? ` ${branch}` : ""}...`);
|
|
9266
|
+
const result = await api.createWorktree({
|
|
9267
|
+
body: { ...body, source: "oneshot", oneshot: oneshotConfig }
|
|
9268
|
+
});
|
|
9269
|
+
branch = result.primaryBranch;
|
|
9270
|
+
stdout(`[${timestamp()}] [event] created ${branch}`);
|
|
9271
|
+
}
|
|
9272
|
+
} catch (error) {
|
|
9273
|
+
stderr(`[${timestamp()}] [error] ${formatServerError(error, port)}`);
|
|
9274
|
+
return 1;
|
|
9275
|
+
}
|
|
9276
|
+
if (!branch) {
|
|
9277
|
+
stderr(`[${timestamp()}] [error] could not resolve branch`);
|
|
9278
|
+
return 1;
|
|
9279
|
+
}
|
|
9280
|
+
const ready = await ensureWorktreeReady(branch, port, stderr);
|
|
9281
|
+
if (!ready.ready)
|
|
9282
|
+
return 1;
|
|
9283
|
+
const conversationState = {
|
|
9284
|
+
printedMessageIds: new Set,
|
|
9285
|
+
streamingItemId: null,
|
|
9286
|
+
streamingNeedsHeader: false
|
|
9287
|
+
};
|
|
9288
|
+
try {
|
|
9289
|
+
const initial = await api.fetchAgentsWorktreeConversationHistory({ params: { name: branch } });
|
|
9290
|
+
printConversationHistory(initial, conversationState);
|
|
9291
|
+
} catch {}
|
|
9292
|
+
let resolveExit;
|
|
9293
|
+
const exitPromise = new Promise((resolve3) => {
|
|
9294
|
+
resolveExit = resolve3;
|
|
9295
|
+
});
|
|
9296
|
+
let exiting = false;
|
|
9297
|
+
let stream = null;
|
|
9298
|
+
let historyPoller = null;
|
|
9299
|
+
let poller = null;
|
|
9300
|
+
const finalize = (code) => {
|
|
9301
|
+
if (exiting)
|
|
9302
|
+
return;
|
|
9303
|
+
exiting = true;
|
|
9304
|
+
stream?.close();
|
|
9305
|
+
historyPoller?.stop();
|
|
9306
|
+
poller?.stop();
|
|
9307
|
+
flushStreamingLine(conversationState);
|
|
9308
|
+
resolveExit(code);
|
|
9309
|
+
};
|
|
9310
|
+
stream = streamConversation(branch, port, conversationState, stderr, (reason) => {
|
|
9311
|
+
stderr(`[${timestamp()}] [fatal] ${reason}`);
|
|
9312
|
+
finalize(1);
|
|
9313
|
+
});
|
|
9314
|
+
if (ready.worktree.agentName === "claude") {
|
|
9315
|
+
historyPoller = pollConversationHistory(branch, port, conversationState);
|
|
9316
|
+
}
|
|
9317
|
+
const pollState = {
|
|
9318
|
+
seenPrUrls: new Set,
|
|
9319
|
+
seenMergedUrls: new Set,
|
|
9320
|
+
hadOpenSession: false,
|
|
9321
|
+
consecutiveClosedReadings: 0,
|
|
9322
|
+
idleSinceMs: null,
|
|
9323
|
+
watcherWasArmed: false
|
|
9324
|
+
};
|
|
9325
|
+
poller = pollProjectState(branch, port, pollState, {
|
|
9326
|
+
onSessionClosed: () => {
|
|
9327
|
+
stdout(`[${timestamp()}] [event] session closed \u2014 exiting`);
|
|
9328
|
+
finalize(0);
|
|
9329
|
+
},
|
|
9330
|
+
onWorktreeRemoved: () => {
|
|
9331
|
+
stdout(`[${timestamp()}] [event] worktree removed \u2014 exiting`);
|
|
9332
|
+
finalize(0);
|
|
7946
9333
|
},
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
body: PullMainRequestSchema,
|
|
7951
|
-
responses: {
|
|
7952
|
-
200: PullMainResponseSchema,
|
|
7953
|
-
...commonErrorResponses
|
|
7954
|
-
}
|
|
9334
|
+
onPrEvent: (line) => {
|
|
9335
|
+
flushStreamingLine(conversationState);
|
|
9336
|
+
stdout(`[${timestamp()}] [event] ${line}`);
|
|
7955
9337
|
},
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
responses: {
|
|
7961
|
-
200: CiLogsResponseSchema,
|
|
7962
|
-
...commonErrorResponses
|
|
7963
|
-
}
|
|
9338
|
+
onAgentDone: (reason) => {
|
|
9339
|
+
flushStreamingLine(conversationState);
|
|
9340
|
+
stdout(`[${timestamp()}] [event] ${reason} \u2014 exiting`);
|
|
9341
|
+
finalize(0);
|
|
7964
9342
|
},
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
9343
|
+
onAgentStuck: (reason) => {
|
|
9344
|
+
flushStreamingLine(conversationState);
|
|
9345
|
+
stderr(`[${timestamp()}] [error] ${reason}`);
|
|
9346
|
+
finalize(1);
|
|
9347
|
+
},
|
|
9348
|
+
onUserTookOver: () => {
|
|
9349
|
+
flushStreamingLine(conversationState);
|
|
9350
|
+
stdout(`[${timestamp()}] [event] user took over from the browser \u2014 exiting`);
|
|
9351
|
+
finalize(0);
|
|
9352
|
+
}
|
|
9353
|
+
}, stderr);
|
|
9354
|
+
const onSignal = () => {
|
|
9355
|
+
stdout(`
|
|
9356
|
+
[${timestamp()}] [event] interrupted \u2014 worktree ${branch} keeps running`);
|
|
9357
|
+
stdout(`[${timestamp()}] [event] resume with: webmux oneshot --resume ${branch}`);
|
|
9358
|
+
finalize(130);
|
|
9359
|
+
};
|
|
9360
|
+
process.on("SIGINT", onSignal);
|
|
9361
|
+
process.on("SIGTERM", onSignal);
|
|
9362
|
+
const finalExit = await exitPromise;
|
|
9363
|
+
process.off("SIGINT", onSignal);
|
|
9364
|
+
process.off("SIGTERM", onSignal);
|
|
9365
|
+
return finalExit;
|
|
9366
|
+
}
|
|
9367
|
+
async function runOneshotCommand(args, port) {
|
|
9368
|
+
let parsed;
|
|
9369
|
+
try {
|
|
9370
|
+
parsed = parseOneshotArgs(args);
|
|
9371
|
+
} catch (error) {
|
|
9372
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
9373
|
+
console.error(getOneshotUsage());
|
|
9374
|
+
return 1;
|
|
9375
|
+
}
|
|
9376
|
+
if (!parsed) {
|
|
9377
|
+
console.log(getOneshotUsage());
|
|
9378
|
+
return 0;
|
|
9379
|
+
}
|
|
9380
|
+
return await runOneshot(parsed, port);
|
|
9381
|
+
}
|
|
9382
|
+
var TOOL_PRIMARY_KEY, MAX_CONSECUTIVE_RECONNECTS = 30, RECONNECT_WARN_AT, IDLE_GRACE_MS = 15000;
|
|
9383
|
+
var init_oneshot = __esm(() => {
|
|
9384
|
+
init_src();
|
|
9385
|
+
init_linear_service();
|
|
9386
|
+
init_conversation_export_service();
|
|
9387
|
+
init_shared();
|
|
9388
|
+
TOOL_PRIMARY_KEY = {
|
|
9389
|
+
bash: ["command"],
|
|
9390
|
+
bashoutput: ["bash_id"],
|
|
9391
|
+
killshell: ["shell_id"],
|
|
9392
|
+
read: ["file_path"],
|
|
9393
|
+
edit: ["file_path"],
|
|
9394
|
+
multiedit: ["file_path"],
|
|
9395
|
+
write: ["file_path"],
|
|
9396
|
+
notebookedit: ["notebook_path"],
|
|
9397
|
+
glob: ["pattern"],
|
|
9398
|
+
grep: ["pattern"],
|
|
9399
|
+
webfetch: ["url"],
|
|
9400
|
+
websearch: ["query"],
|
|
9401
|
+
task: ["description", "subagent_type"],
|
|
9402
|
+
exitplanmode: ["plan"]
|
|
9403
|
+
};
|
|
9404
|
+
RECONNECT_WARN_AT = [3, 15];
|
|
7979
9405
|
});
|
|
7980
9406
|
|
|
7981
|
-
//
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
9407
|
+
// bin/src/linear-commands.ts
|
|
9408
|
+
var exports_linear_commands = {};
|
|
9409
|
+
__export(exports_linear_commands, {
|
|
9410
|
+
runLinearCommand: () => runLinearCommand,
|
|
9411
|
+
parseLinearTargetArg: () => parseLinearTargetArg,
|
|
9412
|
+
parseLinearArgs: () => parseLinearArgs,
|
|
9413
|
+
getLinearUsage: () => getLinearUsage
|
|
9414
|
+
});
|
|
9415
|
+
function getLinearUsage() {
|
|
9416
|
+
return [
|
|
9417
|
+
"Usage:",
|
|
9418
|
+
" webmux linear post <branch> <team-key> [--title <text>]",
|
|
9419
|
+
"",
|
|
9420
|
+
"Creates a new Linear issue in <team-key> and posts the worktree's conversation",
|
|
9421
|
+
"as a JSON attachment + summary comment.",
|
|
9422
|
+
"",
|
|
9423
|
+
" <team-key> Linear team key (e.g. ENG). A new issue is created in that team.",
|
|
9424
|
+
" --title <text> Override the auto-derived title for the new issue",
|
|
9425
|
+
"",
|
|
9426
|
+
"To post into an existing issue, start the session with `webmux oneshot --linear",
|
|
9427
|
+
"<issue-id>` or `webmux add --from-linear <issue-id>` so the issue is the seed.",
|
|
9428
|
+
"",
|
|
9429
|
+
"Examples:",
|
|
9430
|
+
" webmux linear post feat/foo ENG",
|
|
9431
|
+
' webmux linear post feat/foo ENG --title "Investigate flaky test"'
|
|
9432
|
+
].join(`
|
|
9433
|
+
`);
|
|
7992
9434
|
}
|
|
7993
|
-
function
|
|
7994
|
-
|
|
9435
|
+
function readOptionValue2(args, index, flag) {
|
|
9436
|
+
const arg = args[index];
|
|
9437
|
+
if (!arg)
|
|
9438
|
+
throw new CommandUsageError(`${flag} requires a value`);
|
|
9439
|
+
const prefix = `${flag}=`;
|
|
9440
|
+
if (arg.startsWith(prefix))
|
|
9441
|
+
return { value: arg.slice(prefix.length), nextIndex: index };
|
|
9442
|
+
const value = args[index + 1];
|
|
9443
|
+
if (value === undefined)
|
|
9444
|
+
throw new CommandUsageError(`${flag} requires a value`);
|
|
9445
|
+
return { value, nextIndex: index + 1 };
|
|
7995
9446
|
}
|
|
7996
|
-
function
|
|
7997
|
-
|
|
7998
|
-
|
|
9447
|
+
function parseLinearTargetArg(raw) {
|
|
9448
|
+
const target = parseLinearTarget(raw);
|
|
9449
|
+
if (target.kind === "team") {
|
|
9450
|
+
return { kind: "team", teamKey: target.teamKey };
|
|
7999
9451
|
}
|
|
8000
|
-
if (
|
|
8001
|
-
throw new
|
|
9452
|
+
if (target.kind === "issue") {
|
|
9453
|
+
throw new CommandUsageError(`Post target must be a team key (e.g. ENG). To post to issue ${target.issueId} as part of a session, use --linear ${target.issueId} on the oneshot/add command (loads issue context and posts back to it).`);
|
|
8002
9454
|
}
|
|
8003
|
-
|
|
9455
|
+
throw new CommandUsageError(`Invalid Linear team key "${target.raw}". Use a team key like ENG.`);
|
|
8004
9456
|
}
|
|
8005
|
-
function
|
|
8006
|
-
if (
|
|
8007
|
-
|
|
8008
|
-
const parsed = JSON.parse(body);
|
|
8009
|
-
return errorMessageFromResponse(parsed, status2);
|
|
8010
|
-
} catch {
|
|
8011
|
-
return body.trim() || `HTTP ${status2}`;
|
|
8012
|
-
}
|
|
8013
|
-
}
|
|
8014
|
-
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
|
8015
|
-
return body.error;
|
|
9457
|
+
function parseLinearArgs(args) {
|
|
9458
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
9459
|
+
return null;
|
|
8016
9460
|
}
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
const [first, ...rest] = args;
|
|
8021
|
-
if (!first || typeof first !== "object" || !("params" in first) || !first.params || typeof first.params !== "object") {
|
|
8022
|
-
return args;
|
|
9461
|
+
const subcommand = args[0];
|
|
9462
|
+
if (subcommand !== "post") {
|
|
9463
|
+
throw new CommandUsageError(`Unknown linear subcommand: ${subcommand}`);
|
|
8023
9464
|
}
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
if (typeof value === "function") {
|
|
8039
|
-
return [key, wrapRouteCall((...args) => Promise.resolve(Reflect.apply(value, undefined, args)))];
|
|
9465
|
+
let branch = null;
|
|
9466
|
+
let targetRaw = null;
|
|
9467
|
+
let titleOverride = null;
|
|
9468
|
+
for (let index = 1;index < args.length; index++) {
|
|
9469
|
+
const arg = args[index];
|
|
9470
|
+
if (!arg)
|
|
9471
|
+
continue;
|
|
9472
|
+
if (arg === "--help" || arg === "-h")
|
|
9473
|
+
return null;
|
|
9474
|
+
if (arg === "--title" || arg.startsWith("--title=")) {
|
|
9475
|
+
const { value, nextIndex } = readOptionValue2(args, index, "--title");
|
|
9476
|
+
titleOverride = value;
|
|
9477
|
+
index = nextIndex;
|
|
9478
|
+
continue;
|
|
8040
9479
|
}
|
|
8041
|
-
if (
|
|
8042
|
-
|
|
9480
|
+
if (arg.startsWith("-")) {
|
|
9481
|
+
throw new CommandUsageError(`Unknown option: ${arg}`);
|
|
8043
9482
|
}
|
|
8044
|
-
|
|
8045
|
-
|
|
9483
|
+
if (!branch) {
|
|
9484
|
+
branch = arg;
|
|
9485
|
+
continue;
|
|
9486
|
+
}
|
|
9487
|
+
if (!targetRaw) {
|
|
9488
|
+
targetRaw = arg;
|
|
9489
|
+
continue;
|
|
9490
|
+
}
|
|
9491
|
+
throw new CommandUsageError(`Unexpected argument: ${arg}`);
|
|
9492
|
+
}
|
|
9493
|
+
if (!branch)
|
|
9494
|
+
throw new CommandUsageError("linear post requires a <branch> argument");
|
|
9495
|
+
if (!targetRaw)
|
|
9496
|
+
throw new CommandUsageError("linear post requires a <team-key> argument");
|
|
9497
|
+
const baseTarget = parseLinearTargetArg(targetRaw);
|
|
9498
|
+
const target = baseTarget.kind === "team" && titleOverride ? { kind: "team", teamKey: baseTarget.teamKey, title: titleOverride } : baseTarget;
|
|
9499
|
+
return {
|
|
9500
|
+
subcommand: "post",
|
|
9501
|
+
post: { branch, target, titleOverride }
|
|
9502
|
+
};
|
|
8046
9503
|
}
|
|
8047
|
-
function
|
|
8048
|
-
|
|
9504
|
+
async function runLinearCommand(args, port) {
|
|
9505
|
+
let parsed;
|
|
9506
|
+
try {
|
|
9507
|
+
parsed = parseLinearArgs(args);
|
|
9508
|
+
} catch (error) {
|
|
9509
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
9510
|
+
console.error(getLinearUsage());
|
|
9511
|
+
return 1;
|
|
9512
|
+
}
|
|
9513
|
+
if (!parsed) {
|
|
9514
|
+
console.log(getLinearUsage());
|
|
9515
|
+
return 0;
|
|
9516
|
+
}
|
|
9517
|
+
const api = createApi(`http://localhost:${port}`);
|
|
9518
|
+
try {
|
|
9519
|
+
const response = await api.postWorktreeToLinear({
|
|
9520
|
+
params: { name: parsed.post.branch },
|
|
9521
|
+
body: { target: parsed.post.target }
|
|
9522
|
+
});
|
|
9523
|
+
console.log(`Posted to Linear issue: ${response.issueUrl}`);
|
|
9524
|
+
if (response.commentUrl)
|
|
9525
|
+
console.log(`Comment: ${response.commentUrl}`);
|
|
9526
|
+
console.log(`Attachment: ${response.attachmentUrl}`);
|
|
9527
|
+
return 0;
|
|
9528
|
+
} catch (error) {
|
|
9529
|
+
console.error(formatServerError(error, port));
|
|
9530
|
+
return 1;
|
|
9531
|
+
}
|
|
8049
9532
|
}
|
|
8050
|
-
var
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
});
|
|
8054
|
-
|
|
8055
|
-
// packages/api-contract/src/index.ts
|
|
8056
|
-
var init_src = __esm(() => {
|
|
8057
|
-
init_contract();
|
|
8058
|
-
init_client();
|
|
8059
|
-
init_schemas();
|
|
9533
|
+
var init_linear_commands = __esm(() => {
|
|
9534
|
+
init_src();
|
|
9535
|
+
init_shared();
|
|
8060
9536
|
});
|
|
8061
9537
|
|
|
8062
9538
|
// backend/src/domain/model.ts
|
|
@@ -10113,7 +11589,7 @@ var require_merge = __commonJS((exports) => {
|
|
|
10113
11589
|
|
|
10114
11590
|
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/addPairToJSMap.js
|
|
10115
11591
|
var require_addPairToJSMap = __commonJS((exports) => {
|
|
10116
|
-
var
|
|
11592
|
+
var log2 = require_log();
|
|
10117
11593
|
var merge = require_merge();
|
|
10118
11594
|
var stringify = require_stringify();
|
|
10119
11595
|
var identity = require_identity();
|
|
@@ -10162,7 +11638,7 @@ var require_addPairToJSMap = __commonJS((exports) => {
|
|
|
10162
11638
|
let jsonStr = JSON.stringify(strKey);
|
|
10163
11639
|
if (jsonStr.length > 40)
|
|
10164
11640
|
jsonStr = jsonStr.substring(0, 36) + '..."';
|
|
10165
|
-
|
|
11641
|
+
log2.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
|
|
10166
11642
|
ctx.mapKeyWarned = true;
|
|
10167
11643
|
}
|
|
10168
11644
|
return strKey;
|
|
@@ -11360,13 +12836,13 @@ var require_timestamp = __commonJS((exports) => {
|
|
|
11360
12836
|
resolve: (str) => parseSexagesimal(str, false),
|
|
11361
12837
|
stringify: stringifySexagesimal
|
|
11362
12838
|
};
|
|
11363
|
-
var
|
|
12839
|
+
var timestamp2 = {
|
|
11364
12840
|
identify: (value) => value instanceof Date,
|
|
11365
12841
|
default: true,
|
|
11366
12842
|
tag: "tag:yaml.org,2002:timestamp",
|
|
11367
12843
|
test: RegExp("^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})" + "(?:" + "(?:t|T|[ \\t]+)" + "([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)" + "(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?" + ")?$"),
|
|
11368
12844
|
resolve(str) {
|
|
11369
|
-
const match = str.match(
|
|
12845
|
+
const match = str.match(timestamp2.test);
|
|
11370
12846
|
if (!match)
|
|
11371
12847
|
throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd");
|
|
11372
12848
|
const [, year, month, day, hour, minute, second] = match.map(Number);
|
|
@@ -11385,7 +12861,7 @@ var require_timestamp = __commonJS((exports) => {
|
|
|
11385
12861
|
};
|
|
11386
12862
|
exports.floatTime = floatTime;
|
|
11387
12863
|
exports.intTime = intTime;
|
|
11388
|
-
exports.timestamp =
|
|
12864
|
+
exports.timestamp = timestamp2;
|
|
11389
12865
|
});
|
|
11390
12866
|
|
|
11391
12867
|
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/schema.js
|
|
@@ -11402,7 +12878,7 @@ var require_schema3 = __commonJS((exports) => {
|
|
|
11402
12878
|
var omap = require_omap();
|
|
11403
12879
|
var pairs = require_pairs();
|
|
11404
12880
|
var set = require_set();
|
|
11405
|
-
var
|
|
12881
|
+
var timestamp2 = require_timestamp();
|
|
11406
12882
|
var schema = [
|
|
11407
12883
|
map.map,
|
|
11408
12884
|
seq.seq,
|
|
@@ -11422,9 +12898,9 @@ var require_schema3 = __commonJS((exports) => {
|
|
|
11422
12898
|
omap.omap,
|
|
11423
12899
|
pairs.pairs,
|
|
11424
12900
|
set.set,
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
12901
|
+
timestamp2.intTime,
|
|
12902
|
+
timestamp2.floatTime,
|
|
12903
|
+
timestamp2.timestamp
|
|
11428
12904
|
];
|
|
11429
12905
|
exports.schema = schema;
|
|
11430
12906
|
});
|
|
@@ -11446,7 +12922,7 @@ var require_tags = __commonJS((exports) => {
|
|
|
11446
12922
|
var pairs = require_pairs();
|
|
11447
12923
|
var schema$2 = require_schema3();
|
|
11448
12924
|
var set = require_set();
|
|
11449
|
-
var
|
|
12925
|
+
var timestamp2 = require_timestamp();
|
|
11450
12926
|
var schemas2 = new Map([
|
|
11451
12927
|
["core", schema.schema],
|
|
11452
12928
|
["failsafe", [map.map, seq.seq, string.string]],
|
|
@@ -11460,11 +12936,11 @@ var require_tags = __commonJS((exports) => {
|
|
|
11460
12936
|
float: float.float,
|
|
11461
12937
|
floatExp: float.floatExp,
|
|
11462
12938
|
floatNaN: float.floatNaN,
|
|
11463
|
-
floatTime:
|
|
12939
|
+
floatTime: timestamp2.floatTime,
|
|
11464
12940
|
int: int.int,
|
|
11465
12941
|
intHex: int.intHex,
|
|
11466
12942
|
intOct: int.intOct,
|
|
11467
|
-
intTime:
|
|
12943
|
+
intTime: timestamp2.intTime,
|
|
11468
12944
|
map: map.map,
|
|
11469
12945
|
merge: merge.merge,
|
|
11470
12946
|
null: _null.nullTag,
|
|
@@ -11472,7 +12948,7 @@ var require_tags = __commonJS((exports) => {
|
|
|
11472
12948
|
pairs: pairs.pairs,
|
|
11473
12949
|
seq: seq.seq,
|
|
11474
12950
|
set: set.set,
|
|
11475
|
-
timestamp:
|
|
12951
|
+
timestamp: timestamp2.timestamp
|
|
11476
12952
|
};
|
|
11477
12953
|
var coreKnownTags = {
|
|
11478
12954
|
"tag:yaml.org,2002:binary": binary.binary,
|
|
@@ -11480,7 +12956,7 @@ var require_tags = __commonJS((exports) => {
|
|
|
11480
12956
|
"tag:yaml.org,2002:omap": omap.omap,
|
|
11481
12957
|
"tag:yaml.org,2002:pairs": pairs.pairs,
|
|
11482
12958
|
"tag:yaml.org,2002:set": set.set,
|
|
11483
|
-
"tag:yaml.org,2002:timestamp":
|
|
12959
|
+
"tag:yaml.org,2002:timestamp": timestamp2.timestamp
|
|
11484
12960
|
};
|
|
11485
12961
|
function getTags(customTags, schemaName, addMergeTag) {
|
|
11486
12962
|
const schemaTags = schemas2.get(schemaName);
|
|
@@ -12748,9 +14224,9 @@ var require_resolve_block_scalar = __commonJS((exports) => {
|
|
|
12748
14224
|
default: {
|
|
12749
14225
|
const message = `Unexpected token in block scalar header: ${token.type}`;
|
|
12750
14226
|
onError(token, "UNEXPECTED_TOKEN", message);
|
|
12751
|
-
const
|
|
12752
|
-
if (
|
|
12753
|
-
length +=
|
|
14227
|
+
const ts2 = token.source;
|
|
14228
|
+
if (ts2 && typeof ts2 === "string")
|
|
14229
|
+
length += ts2.length;
|
|
12754
14230
|
}
|
|
12755
14231
|
}
|
|
12756
14232
|
}
|
|
@@ -13053,9 +14529,9 @@ var require_compose_scalar = __commonJS((exports) => {
|
|
|
13053
14529
|
if (schema.compat) {
|
|
13054
14530
|
const compat = schema.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema[identity.SCALAR];
|
|
13055
14531
|
if (tag.tag !== compat.tag) {
|
|
13056
|
-
const
|
|
14532
|
+
const ts2 = directives.tagString(tag.tag);
|
|
13057
14533
|
const cs = directives.tagString(compat.tag);
|
|
13058
|
-
const msg = `Value may be parsed as either ${
|
|
14534
|
+
const msg = `Value may be parsed as either ${ts2} or ${cs}`;
|
|
13059
14535
|
onError(token, "TAG_RESOLVE_FAILED", msg, true);
|
|
13060
14536
|
}
|
|
13061
14537
|
}
|
|
@@ -15319,7 +16795,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
15319
16795
|
var composer = require_composer();
|
|
15320
16796
|
var Document = require_Document();
|
|
15321
16797
|
var errors2 = require_errors();
|
|
15322
|
-
var
|
|
16798
|
+
var log2 = require_log();
|
|
15323
16799
|
var identity = require_identity();
|
|
15324
16800
|
var lineCounter = require_line_counter();
|
|
15325
16801
|
var parser = require_parser();
|
|
@@ -15371,7 +16847,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
15371
16847
|
const doc = parseDocument(src, options);
|
|
15372
16848
|
if (!doc)
|
|
15373
16849
|
return null;
|
|
15374
|
-
doc.warnings.forEach((warning) =>
|
|
16850
|
+
doc.warnings.forEach((warning) => log2.warn(doc.options.logLevel, warning));
|
|
15375
16851
|
if (doc.errors.length > 0) {
|
|
15376
16852
|
if (doc.options.logLevel !== "silent")
|
|
15377
16853
|
throw doc.errors[0];
|
|
@@ -15460,6 +16936,15 @@ var init_dist5 = __esm(() => {
|
|
|
15460
16936
|
// backend/src/adapters/config.ts
|
|
15461
16937
|
import { readFileSync as readFileSync3 } from "fs";
|
|
15462
16938
|
import { dirname as dirname2, join as join7, resolve as resolve5 } from "path";
|
|
16939
|
+
function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
|
|
16940
|
+
return [
|
|
16941
|
+
"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.",
|
|
16942
|
+
"Your job is to take the task to its real conclusion without pausing:",
|
|
16943
|
+
"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.",
|
|
16944
|
+
"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.",
|
|
16945
|
+
"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)."
|
|
16946
|
+
].join(" ");
|
|
16947
|
+
}
|
|
15463
16948
|
function clonePanes(panes) {
|
|
15464
16949
|
return panes.map((pane) => ({ ...pane }));
|
|
15465
16950
|
}
|
|
@@ -15636,6 +17121,12 @@ function parseLifecycleHooks(raw) {
|
|
|
15636
17121
|
}
|
|
15637
17122
|
return hooks;
|
|
15638
17123
|
}
|
|
17124
|
+
function parseOneshot(raw) {
|
|
17125
|
+
if (!isRecord4(raw))
|
|
17126
|
+
return { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() };
|
|
17127
|
+
const systemPrompt = typeof raw.systemPrompt === "string" && raw.systemPrompt.trim() ? raw.systemPrompt.trim() : DEFAULT_ONESHOT_SYSTEM_PROMPT();
|
|
17128
|
+
return { systemPrompt };
|
|
17129
|
+
}
|
|
15639
17130
|
function parseAutoName(raw) {
|
|
15640
17131
|
if (!isRecord4(raw))
|
|
15641
17132
|
return null;
|
|
@@ -15708,7 +17199,8 @@ function parseProjectConfig(parsed) {
|
|
|
15708
17199
|
}
|
|
15709
17200
|
},
|
|
15710
17201
|
lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
|
|
15711
|
-
autoName: parseAutoName(parsed.auto_name)
|
|
17202
|
+
autoName: parseAutoName(parsed.auto_name),
|
|
17203
|
+
oneshot: parseOneshot(parsed.oneshot)
|
|
15712
17204
|
};
|
|
15713
17205
|
}
|
|
15714
17206
|
function defaultConfig() {
|
|
@@ -15875,7 +17367,8 @@ var init_config = __esm(() => {
|
|
|
15875
17367
|
linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
|
|
15876
17368
|
},
|
|
15877
17369
|
lifecycleHooks: {},
|
|
15878
|
-
autoName: null
|
|
17370
|
+
autoName: null,
|
|
17371
|
+
oneshot: { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() }
|
|
15879
17372
|
};
|
|
15880
17373
|
});
|
|
15881
17374
|
|
|
@@ -15902,30 +17395,6 @@ var init_control_token = __esm(() => {
|
|
|
15902
17395
|
CONTROL_TOKEN_PATH = `${Bun.env.HOME ?? "/root"}/.config/webmux/control-token`;
|
|
15903
17396
|
});
|
|
15904
17397
|
|
|
15905
|
-
// backend/src/lib/log.ts
|
|
15906
|
-
function ts() {
|
|
15907
|
-
return new Date().toISOString().slice(11, 23);
|
|
15908
|
-
}
|
|
15909
|
-
var DEBUG, log;
|
|
15910
|
-
var init_log = __esm(() => {
|
|
15911
|
-
DEBUG = Bun.env.WEBMUX_DEBUG === "1";
|
|
15912
|
-
log = {
|
|
15913
|
-
info(msg) {
|
|
15914
|
-
console.log(`[${ts()}] ${msg}`);
|
|
15915
|
-
},
|
|
15916
|
-
debug(msg) {
|
|
15917
|
-
if (DEBUG)
|
|
15918
|
-
console.log(`[${ts()}] ${msg}`);
|
|
15919
|
-
},
|
|
15920
|
-
warn(msg) {
|
|
15921
|
-
console.warn(`[${ts()}] ${msg}`);
|
|
15922
|
-
},
|
|
15923
|
-
error(msg, err) {
|
|
15924
|
-
err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
|
|
15925
|
-
}
|
|
15926
|
-
};
|
|
15927
|
-
});
|
|
15928
|
-
|
|
15929
17398
|
// backend/src/adapters/docker.ts
|
|
15930
17399
|
import { stat } from "fs/promises";
|
|
15931
17400
|
async function pathExists(p2) {
|
|
@@ -17019,23 +18488,22 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
|
17019
18488
|
return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
|
|
17020
18489
|
}
|
|
17021
18490
|
function buildBuiltInAgentInvocation(input) {
|
|
18491
|
+
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
17022
18492
|
if (input.agent === "codex") {
|
|
17023
18493
|
const hooksFlag = " --enable codex_hooks";
|
|
17024
18494
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
17025
18495
|
if (input.launchMode === "resume") {
|
|
17026
|
-
return `codex${hooksFlag}${yoloFlag2} resume --last`;
|
|
18496
|
+
return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
|
|
17027
18497
|
}
|
|
17028
|
-
const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
17029
18498
|
if (input.systemPrompt) {
|
|
17030
|
-
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${
|
|
18499
|
+
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
|
|
17031
18500
|
}
|
|
17032
|
-
return `codex${hooksFlag}${yoloFlag2}${
|
|
18501
|
+
return `codex${hooksFlag}${yoloFlag2}${promptSuffix}`;
|
|
17033
18502
|
}
|
|
17034
18503
|
const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
|
|
17035
18504
|
if (input.launchMode === "resume") {
|
|
17036
|
-
return `claude${yoloFlag} --continue`;
|
|
18505
|
+
return `claude${yoloFlag} --continue${promptSuffix}`;
|
|
17037
18506
|
}
|
|
17038
|
-
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
17039
18507
|
if (input.systemPrompt) {
|
|
17040
18508
|
return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
|
|
17041
18509
|
}
|
|
@@ -17353,7 +18821,9 @@ async function initializeManagedWorktree(opts) {
|
|
|
17353
18821
|
agent: opts.agent,
|
|
17354
18822
|
runtime: opts.runtime,
|
|
17355
18823
|
startupEnvValues: { ...opts.startupEnvValues ?? {} },
|
|
17356
|
-
allocatedPorts: { ...opts.allocatedPorts ?? {} }
|
|
18824
|
+
allocatedPorts: { ...opts.allocatedPorts ?? {} },
|
|
18825
|
+
...opts.source ? { source: opts.source } : {},
|
|
18826
|
+
...opts.oneshot ? { oneshot: opts.oneshot } : {}
|
|
17357
18827
|
};
|
|
17358
18828
|
const paths = await ensureWorktreeStorageDirs(opts.gitDir);
|
|
17359
18829
|
await writeWorktreeMeta(opts.gitDir, meta);
|
|
@@ -17406,7 +18876,9 @@ async function createManagedWorktree(opts, deps2 = {}) {
|
|
|
17406
18876
|
controlUrl: opts.controlUrl,
|
|
17407
18877
|
controlToken: opts.controlToken,
|
|
17408
18878
|
now: opts.now,
|
|
17409
|
-
worktreeId: opts.worktreeId
|
|
18879
|
+
worktreeId: opts.worktreeId,
|
|
18880
|
+
...opts.source ? { source: opts.source } : {},
|
|
18881
|
+
...opts.oneshot ? { oneshot: opts.oneshot } : {}
|
|
17410
18882
|
});
|
|
17411
18883
|
if (deps2.tmux) {
|
|
17412
18884
|
sessionLayoutPlan = sessionLayoutPlan ?? opts.sessionLayoutPlanBuilder?.(initialized);
|
|
@@ -17546,10 +19018,15 @@ class LifecycleService {
|
|
|
17546
19018
|
agent: agent.id
|
|
17547
19019
|
});
|
|
17548
19020
|
}
|
|
17549
|
-
async openWorktree(branch) {
|
|
19021
|
+
async openWorktree(branch, options = {}) {
|
|
17550
19022
|
try {
|
|
17551
19023
|
const resolved = await this.resolveExistingWorktree(branch);
|
|
17552
|
-
|
|
19024
|
+
let initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
|
|
19025
|
+
if (options.oneshot) {
|
|
19026
|
+
const nextMeta = { ...initialized.meta, oneshot: options.oneshot };
|
|
19027
|
+
await writeWorktreeMeta(initialized.paths.gitDir, nextMeta);
|
|
19028
|
+
initialized = { ...initialized, meta: nextMeta };
|
|
19029
|
+
}
|
|
17553
19030
|
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
17554
19031
|
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
17555
19032
|
const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
|
|
@@ -17564,7 +19041,8 @@ class LifecycleService {
|
|
|
17564
19041
|
agent,
|
|
17565
19042
|
initialized,
|
|
17566
19043
|
worktreePath: resolved.entry.path,
|
|
17567
|
-
launchMode
|
|
19044
|
+
launchMode,
|
|
19045
|
+
followUpPrompt: options.prompt
|
|
17568
19046
|
});
|
|
17569
19047
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
17570
19048
|
return {
|
|
@@ -17575,6 +19053,20 @@ class LifecycleService {
|
|
|
17575
19053
|
throw this.wrapOperationError(error);
|
|
17576
19054
|
}
|
|
17577
19055
|
}
|
|
19056
|
+
async disarmOneshot(branch) {
|
|
19057
|
+
let resolved;
|
|
19058
|
+
try {
|
|
19059
|
+
resolved = await this.resolveExistingWorktree(branch);
|
|
19060
|
+
} catch {
|
|
19061
|
+
return false;
|
|
19062
|
+
}
|
|
19063
|
+
if (!resolved.meta?.oneshot)
|
|
19064
|
+
return false;
|
|
19065
|
+
const nextMeta = { ...resolved.meta };
|
|
19066
|
+
delete nextMeta.oneshot;
|
|
19067
|
+
await writeWorktreeMeta(resolved.gitDir, nextMeta);
|
|
19068
|
+
return true;
|
|
19069
|
+
}
|
|
17578
19070
|
async closeWorktree(branch) {
|
|
17579
19071
|
try {
|
|
17580
19072
|
await this.resolveExistingWorktree(branch);
|
|
@@ -17754,7 +19246,7 @@ class LifecycleService {
|
|
|
17754
19246
|
}
|
|
17755
19247
|
listProjectWorktrees() {
|
|
17756
19248
|
const projectRoot2 = resolve8(this.deps.projectRoot);
|
|
17757
|
-
return this.deps.git.
|
|
19249
|
+
return this.deps.git.listLiveWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve8(entry.path) !== projectRoot2);
|
|
17758
19250
|
}
|
|
17759
19251
|
async readManagedMetas() {
|
|
17760
19252
|
const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
|
|
@@ -17861,8 +19353,10 @@ class LifecycleService {
|
|
|
17861
19353
|
agent: input.agent,
|
|
17862
19354
|
initialized: input.initialized,
|
|
17863
19355
|
worktreePath: input.worktreePath,
|
|
17864
|
-
|
|
19356
|
+
creationPrompt: input.creationPrompt,
|
|
19357
|
+
followUpPrompt: input.followUpPrompt,
|
|
17865
19358
|
launchMode: input.launchMode,
|
|
19359
|
+
source: input.source,
|
|
17866
19360
|
containerName: containerName2
|
|
17867
19361
|
}));
|
|
17868
19362
|
return;
|
|
@@ -17874,12 +19368,19 @@ class LifecycleService {
|
|
|
17874
19368
|
agent: input.agent,
|
|
17875
19369
|
initialized: input.initialized,
|
|
17876
19370
|
worktreePath: input.worktreePath,
|
|
17877
|
-
|
|
17878
|
-
|
|
19371
|
+
creationPrompt: input.creationPrompt,
|
|
19372
|
+
followUpPrompt: input.followUpPrompt,
|
|
19373
|
+
launchMode: input.launchMode,
|
|
19374
|
+
source: input.source
|
|
17879
19375
|
}));
|
|
17880
19376
|
}
|
|
17881
19377
|
buildSessionLayout(input) {
|
|
17882
|
-
const
|
|
19378
|
+
const baseSystemPrompt = input.launchMode === "fresh" && input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
|
|
19379
|
+
const oneshotPrompt = input.launchMode === "fresh" && input.source === "oneshot" ? this.deps.config.oneshot.systemPrompt : undefined;
|
|
19380
|
+
const systemPrompt = baseSystemPrompt && oneshotPrompt ? `${baseSystemPrompt}
|
|
19381
|
+
|
|
19382
|
+
${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
|
|
19383
|
+
const prompt = input.launchMode === "resume" ? input.followUpPrompt : input.creationPrompt;
|
|
17883
19384
|
const containerName2 = input.containerName;
|
|
17884
19385
|
return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
|
|
17885
19386
|
repoRoot: this.deps.projectRoot,
|
|
@@ -17894,7 +19395,7 @@ class LifecycleService {
|
|
|
17894
19395
|
profileName: input.profileName,
|
|
17895
19396
|
yolo: input.profile.yolo === true,
|
|
17896
19397
|
systemPrompt,
|
|
17897
|
-
prompt
|
|
19398
|
+
prompt,
|
|
17898
19399
|
launchMode: input.launchMode
|
|
17899
19400
|
}),
|
|
17900
19401
|
shell: buildDockerShellCommand(containerName2, input.worktreePath, input.initialized.paths.runtimeEnvPath)
|
|
@@ -17908,7 +19409,7 @@ class LifecycleService {
|
|
|
17908
19409
|
profileName: input.profileName,
|
|
17909
19410
|
yolo: input.profile.yolo === true,
|
|
17910
19411
|
systemPrompt,
|
|
17911
|
-
prompt
|
|
19412
|
+
prompt,
|
|
17912
19413
|
launchMode: input.launchMode
|
|
17913
19414
|
}),
|
|
17914
19415
|
shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
|
|
@@ -18032,12 +19533,14 @@ class LifecycleService {
|
|
|
18032
19533
|
const { profileName, profile } = this.resolveProfile(input.profile);
|
|
18033
19534
|
const agent = this.resolveAgentDefinition(input.agent);
|
|
18034
19535
|
const worktreePath = this.resolveWorktreePath(input.branch);
|
|
19536
|
+
const source = input.source ?? "ui";
|
|
18035
19537
|
const createProgressBase = {
|
|
18036
19538
|
branch: input.branch,
|
|
18037
19539
|
...baseBranch ? { baseBranch } : {},
|
|
18038
19540
|
path: worktreePath,
|
|
18039
19541
|
profile: profileName,
|
|
18040
|
-
agent: input.agent
|
|
19542
|
+
agent: input.agent,
|
|
19543
|
+
source
|
|
18041
19544
|
};
|
|
18042
19545
|
const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
|
|
18043
19546
|
let initialized = null;
|
|
@@ -18062,7 +19565,9 @@ class LifecycleService {
|
|
|
18062
19565
|
runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
|
|
18063
19566
|
controlUrl: this.controlUrl(profile.runtime),
|
|
18064
19567
|
controlToken: await this.deps.getControlToken(),
|
|
18065
|
-
deleteBranchOnRollback
|
|
19568
|
+
deleteBranchOnRollback,
|
|
19569
|
+
source,
|
|
19570
|
+
...input.oneshot ? { oneshot: input.oneshot } : {}
|
|
18066
19571
|
}, {
|
|
18067
19572
|
git: this.deps.git
|
|
18068
19573
|
});
|
|
@@ -18100,8 +19605,9 @@ class LifecycleService {
|
|
|
18100
19605
|
agent,
|
|
18101
19606
|
initialized,
|
|
18102
19607
|
worktreePath,
|
|
18103
|
-
|
|
18104
|
-
launchMode: "fresh"
|
|
19608
|
+
creationPrompt: input.prompt,
|
|
19609
|
+
launchMode: "fresh",
|
|
19610
|
+
source
|
|
18105
19611
|
});
|
|
18106
19612
|
await this.reportCreateProgress({
|
|
18107
19613
|
...createProgressBase,
|
|
@@ -18263,6 +19769,8 @@ function makeDefaultState(input) {
|
|
|
18263
19769
|
path: input.path,
|
|
18264
19770
|
profile: input.profile ?? null,
|
|
18265
19771
|
agentName: input.agentName ?? null,
|
|
19772
|
+
source: input.source ?? "ui",
|
|
19773
|
+
oneshot: input.oneshot ?? null,
|
|
18266
19774
|
git: {
|
|
18267
19775
|
exists: true,
|
|
18268
19776
|
branch: input.branch,
|
|
@@ -18312,6 +19820,10 @@ class ProjectRuntime {
|
|
|
18312
19820
|
existing.agentName = input.agentName ?? existing.agentName;
|
|
18313
19821
|
if (input.runtime)
|
|
18314
19822
|
existing.agent.runtime = input.runtime;
|
|
19823
|
+
if (input.source !== undefined)
|
|
19824
|
+
existing.source = input.source;
|
|
19825
|
+
if (input.oneshot !== undefined)
|
|
19826
|
+
existing.oneshot = input.oneshot;
|
|
18315
19827
|
existing.git.exists = true;
|
|
18316
19828
|
existing.git.branch = input.branch;
|
|
18317
19829
|
existing.session.windowName = buildWorktreeWindowName(input.branch);
|
|
@@ -18322,6 +19834,11 @@ class ProjectRuntime {
|
|
|
18322
19834
|
this.worktreeIdsByBranch.set(input.branch, input.worktreeId);
|
|
18323
19835
|
return created;
|
|
18324
19836
|
}
|
|
19837
|
+
setOneshot(worktreeId, oneshot) {
|
|
19838
|
+
const state = this.requireWorktree(worktreeId);
|
|
19839
|
+
state.oneshot = oneshot;
|
|
19840
|
+
return state;
|
|
19841
|
+
}
|
|
18325
19842
|
removeWorktree(worktreeId) {
|
|
18326
19843
|
const state = this.worktrees.get(worktreeId);
|
|
18327
19844
|
if (!state)
|
|
@@ -18367,36 +19884,36 @@ class ProjectRuntime {
|
|
|
18367
19884
|
if (event.branch !== state.branch) {
|
|
18368
19885
|
this.applyBranchChange(state, event.branch);
|
|
18369
19886
|
}
|
|
18370
|
-
const
|
|
19887
|
+
const timestamp2 = isoNow(now);
|
|
18371
19888
|
switch (event.type) {
|
|
18372
19889
|
case "agent_stopped":
|
|
18373
19890
|
state.agent.lifecycle = "stopped";
|
|
18374
|
-
state.agent.lastEventAt =
|
|
19891
|
+
state.agent.lastEventAt = timestamp2;
|
|
18375
19892
|
break;
|
|
18376
19893
|
case "agent_status_changed":
|
|
18377
|
-
this.applyStatusChanged(state, event,
|
|
19894
|
+
this.applyStatusChanged(state, event, timestamp2);
|
|
18378
19895
|
break;
|
|
18379
19896
|
case "runtime_error":
|
|
18380
|
-
this.applyRuntimeError(state, event,
|
|
19897
|
+
this.applyRuntimeError(state, event, timestamp2);
|
|
18381
19898
|
break;
|
|
18382
19899
|
case "pr_opened":
|
|
18383
|
-
state.agent.lastEventAt =
|
|
19900
|
+
state.agent.lastEventAt = timestamp2;
|
|
18384
19901
|
break;
|
|
18385
19902
|
}
|
|
18386
19903
|
return state;
|
|
18387
19904
|
}
|
|
18388
|
-
applyStatusChanged(state, event,
|
|
19905
|
+
applyStatusChanged(state, event, timestamp2) {
|
|
18389
19906
|
state.agent.lifecycle = event.lifecycle;
|
|
18390
|
-
state.agent.lastEventAt =
|
|
19907
|
+
state.agent.lastEventAt = timestamp2;
|
|
18391
19908
|
if (state.agent.lastStartedAt === null && event.lifecycle === "running") {
|
|
18392
|
-
state.agent.lastStartedAt =
|
|
19909
|
+
state.agent.lastStartedAt = timestamp2;
|
|
18393
19910
|
}
|
|
18394
19911
|
state.agent.lastError = null;
|
|
18395
19912
|
}
|
|
18396
|
-
applyRuntimeError(state, event,
|
|
19913
|
+
applyRuntimeError(state, event, timestamp2) {
|
|
18397
19914
|
state.agent.lifecycle = "error";
|
|
18398
19915
|
state.agent.lastError = event.message;
|
|
18399
|
-
state.agent.lastEventAt =
|
|
19916
|
+
state.agent.lastEventAt = timestamp2;
|
|
18400
19917
|
}
|
|
18401
19918
|
applyBranchChange(state, branch) {
|
|
18402
19919
|
this.reindexBranch(state.branch, branch, state.worktreeId);
|
|
@@ -18507,7 +20024,7 @@ class ReconciliationService {
|
|
|
18507
20024
|
return await this.inFlight;
|
|
18508
20025
|
}
|
|
18509
20026
|
async runReconcile(normalizedRepoRoot) {
|
|
18510
|
-
const worktrees = this.deps.git.
|
|
20027
|
+
const worktrees = this.deps.git.listLiveWorktrees(normalizedRepoRoot);
|
|
18511
20028
|
const sessionName = buildProjectSessionName(normalizedRepoRoot);
|
|
18512
20029
|
let windows = [];
|
|
18513
20030
|
try {
|
|
@@ -18533,6 +20050,8 @@ class ReconciliationService {
|
|
|
18533
20050
|
profile: meta?.profile ?? null,
|
|
18534
20051
|
agentName: meta?.agent ?? null,
|
|
18535
20052
|
runtime: meta?.runtime ?? "host",
|
|
20053
|
+
source: meta?.source ?? "ui",
|
|
20054
|
+
oneshot: meta?.oneshot ?? null,
|
|
18536
20055
|
git: {
|
|
18537
20056
|
dirty: gitStatus.dirty,
|
|
18538
20057
|
aheadCount: gitStatus.aheadCount,
|
|
@@ -18565,7 +20084,9 @@ class ReconciliationService {
|
|
|
18565
20084
|
path: state.path,
|
|
18566
20085
|
profile: state.profile,
|
|
18567
20086
|
agentName: state.agentName,
|
|
18568
|
-
runtime: state.runtime
|
|
20087
|
+
runtime: state.runtime,
|
|
20088
|
+
source: state.source,
|
|
20089
|
+
oneshot: state.oneshot
|
|
18569
20090
|
});
|
|
18570
20091
|
this.deps.runtime.setGitState(state.worktreeId, {
|
|
18571
20092
|
exists: true,
|
|
@@ -18605,7 +20126,8 @@ class WorktreeCreationTracker {
|
|
|
18605
20126
|
path: progress.path,
|
|
18606
20127
|
profile: progress.profile,
|
|
18607
20128
|
agentName: progress.agent,
|
|
18608
|
-
phase: progress.phase
|
|
20129
|
+
phase: progress.phase,
|
|
20130
|
+
source: progress.source
|
|
18609
20131
|
};
|
|
18610
20132
|
this.worktrees.set(progress.branch, next);
|
|
18611
20133
|
}
|
|
@@ -18711,7 +20233,7 @@ function getWorktreeCommandUsage(command) {
|
|
|
18711
20233
|
case "add":
|
|
18712
20234
|
return [
|
|
18713
20235
|
"Usage:",
|
|
18714
|
-
" webmux add [branch] [--existing] [--base <branch>] [--profile <name>] [--agent <id>] [--prompt <text>] [--env KEY=VALUE] [--detach]",
|
|
20236
|
+
" webmux add [branch] [--existing] [--base <branch>] [--profile <name>] [--agent <id>] [--prompt <text>] [--env KEY=VALUE] [--detach] [--from-linear <issue-id>]",
|
|
18715
20237
|
"",
|
|
18716
20238
|
"Options:",
|
|
18717
20239
|
" --existing Use an existing local or remote branch instead of creating a new one",
|
|
@@ -18721,6 +20243,8 @@ function getWorktreeCommandUsage(command) {
|
|
|
18721
20243
|
" --prompt <text> Initial agent prompt",
|
|
18722
20244
|
" --env KEY=VALUE Runtime env override (repeatable)",
|
|
18723
20245
|
" -d, --detach Create worktree without switching to it",
|
|
20246
|
+
" --from-linear ID Bootstrap from a Linear issue \u2014 loads the issue body as",
|
|
20247
|
+
" context, plus any saved webmux session or linked PR",
|
|
18724
20248
|
" --help Show this help message"
|
|
18725
20249
|
].join(`
|
|
18726
20250
|
`);
|
|
@@ -18782,7 +20306,7 @@ function getWorktreeCommandUsage(command) {
|
|
|
18782
20306
|
webmux prune`;
|
|
18783
20307
|
}
|
|
18784
20308
|
}
|
|
18785
|
-
function
|
|
20309
|
+
function readOptionValue3(args, index, flag) {
|
|
18786
20310
|
const arg = args[index];
|
|
18787
20311
|
if (!arg) {
|
|
18788
20312
|
throw new CommandUsageError(`${flag} requires a value`);
|
|
@@ -18815,6 +20339,8 @@ function parseAddCommandArgs(args) {
|
|
|
18815
20339
|
const envOverrides = {};
|
|
18816
20340
|
const selectedAgents = [];
|
|
18817
20341
|
let detach = false;
|
|
20342
|
+
let fromLinearIssueId = null;
|
|
20343
|
+
let branchExplicit = false;
|
|
18818
20344
|
for (let index = 0;index < args.length; index++) {
|
|
18819
20345
|
const arg = args[index];
|
|
18820
20346
|
if (!arg)
|
|
@@ -18831,31 +20357,31 @@ function parseAddCommandArgs(args) {
|
|
|
18831
20357
|
continue;
|
|
18832
20358
|
}
|
|
18833
20359
|
if (arg === "--profile" || arg.startsWith("--profile=")) {
|
|
18834
|
-
const { value, nextIndex } =
|
|
20360
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--profile");
|
|
18835
20361
|
input.profile = value;
|
|
18836
20362
|
index = nextIndex;
|
|
18837
20363
|
continue;
|
|
18838
20364
|
}
|
|
18839
20365
|
if (arg === "--base" || arg.startsWith("--base=")) {
|
|
18840
|
-
const { value, nextIndex } =
|
|
20366
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--base");
|
|
18841
20367
|
input.baseBranch = value;
|
|
18842
20368
|
index = nextIndex;
|
|
18843
20369
|
continue;
|
|
18844
20370
|
}
|
|
18845
20371
|
if (arg === "--agent" || arg.startsWith("--agent=")) {
|
|
18846
|
-
const { value, nextIndex } =
|
|
20372
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--agent");
|
|
18847
20373
|
selectedAgents.push(parseAgent(value));
|
|
18848
20374
|
index = nextIndex;
|
|
18849
20375
|
continue;
|
|
18850
20376
|
}
|
|
18851
20377
|
if (arg === "--prompt" || arg.startsWith("--prompt=")) {
|
|
18852
|
-
const { value, nextIndex } =
|
|
20378
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--prompt");
|
|
18853
20379
|
input.prompt = value;
|
|
18854
20380
|
index = nextIndex;
|
|
18855
20381
|
continue;
|
|
18856
20382
|
}
|
|
18857
20383
|
if (arg === "--env" || arg.startsWith("--env=")) {
|
|
18858
|
-
const { value, nextIndex } =
|
|
20384
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--env");
|
|
18859
20385
|
const separatorIndex = value.indexOf("=");
|
|
18860
20386
|
if (separatorIndex <= 0) {
|
|
18861
20387
|
throw new CommandUsageError("--env must use KEY=VALUE");
|
|
@@ -18864,6 +20390,26 @@ function parseAddCommandArgs(args) {
|
|
|
18864
20390
|
index = nextIndex;
|
|
18865
20391
|
continue;
|
|
18866
20392
|
}
|
|
20393
|
+
if (arg === "--from-linear" || arg.startsWith("--from-linear=")) {
|
|
20394
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--from-linear");
|
|
20395
|
+
const trimmed = value.trim();
|
|
20396
|
+
if (!/^[A-Z]+-\d+$/.test(trimmed)) {
|
|
20397
|
+
throw new CommandUsageError(`--from-linear expects an issue id like ENG-123 (got "${trimmed}")`);
|
|
20398
|
+
}
|
|
20399
|
+
fromLinearIssueId = trimmed;
|
|
20400
|
+
index = nextIndex;
|
|
20401
|
+
continue;
|
|
20402
|
+
}
|
|
20403
|
+
if (arg === "--branch" || arg.startsWith("--branch=")) {
|
|
20404
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--branch");
|
|
20405
|
+
if (input.branch && input.branch !== value) {
|
|
20406
|
+
throw new CommandUsageError(`Conflicting branch values: "${input.branch}" and "${value}"`);
|
|
20407
|
+
}
|
|
20408
|
+
input.branch = value.trim();
|
|
20409
|
+
branchExplicit = true;
|
|
20410
|
+
index = nextIndex;
|
|
20411
|
+
continue;
|
|
20412
|
+
}
|
|
18867
20413
|
if (arg.startsWith("-")) {
|
|
18868
20414
|
throw new CommandUsageError(`Unknown option: ${arg}`);
|
|
18869
20415
|
}
|
|
@@ -18871,6 +20417,7 @@ function parseAddCommandArgs(args) {
|
|
|
18871
20417
|
throw new CommandUsageError(`Unexpected argument: ${arg}`);
|
|
18872
20418
|
}
|
|
18873
20419
|
input.branch = arg;
|
|
20420
|
+
branchExplicit = true;
|
|
18874
20421
|
}
|
|
18875
20422
|
if (selectedAgents.length > 0) {
|
|
18876
20423
|
input.agents = selectedAgents;
|
|
@@ -18878,7 +20425,7 @@ function parseAddCommandArgs(args) {
|
|
|
18878
20425
|
if (Object.keys(envOverrides).length > 0) {
|
|
18879
20426
|
input.envOverrides = envOverrides;
|
|
18880
20427
|
}
|
|
18881
|
-
return { input, detach };
|
|
20428
|
+
return { input, detach, fromLinearIssueId, branchExplicit };
|
|
18882
20429
|
}
|
|
18883
20430
|
function parseBranchCommandArgs(args) {
|
|
18884
20431
|
let branch = null;
|
|
@@ -18922,7 +20469,7 @@ function parseLabelCommandArgs(args) {
|
|
|
18922
20469
|
if (optionLabel !== null) {
|
|
18923
20470
|
throw new CommandUsageError("Cannot use --label more than once");
|
|
18924
20471
|
}
|
|
18925
|
-
const { value, nextIndex } =
|
|
20472
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--label");
|
|
18926
20473
|
optionLabel = value;
|
|
18927
20474
|
index = nextIndex;
|
|
18928
20475
|
continue;
|
|
@@ -18974,13 +20521,13 @@ function parseSendCommandArgs(args) {
|
|
|
18974
20521
|
if (arg === "--prompt" || arg.startsWith("--prompt=")) {
|
|
18975
20522
|
if (text)
|
|
18976
20523
|
throw new CommandUsageError("Cannot use --prompt with a positional prompt argument");
|
|
18977
|
-
const { value, nextIndex } =
|
|
20524
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--prompt");
|
|
18978
20525
|
text = value;
|
|
18979
20526
|
index = nextIndex;
|
|
18980
20527
|
continue;
|
|
18981
20528
|
}
|
|
18982
20529
|
if (arg === "--preamble" || arg.startsWith("--preamble=")) {
|
|
18983
|
-
const { value, nextIndex } =
|
|
20530
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--preamble");
|
|
18984
20531
|
preamble = value;
|
|
18985
20532
|
index = nextIndex;
|
|
18986
20533
|
continue;
|
|
@@ -19046,7 +20593,7 @@ function parseListCommandArgs(args) {
|
|
|
19046
20593
|
continue;
|
|
19047
20594
|
}
|
|
19048
20595
|
if (arg === "--search" || arg.startsWith("--search=")) {
|
|
19049
|
-
const { value, nextIndex } =
|
|
20596
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--search");
|
|
19050
20597
|
search = value;
|
|
19051
20598
|
index = nextIndex;
|
|
19052
20599
|
continue;
|
|
@@ -19190,6 +20737,31 @@ async function runWorktreeCommand(context, deps2 = {}) {
|
|
|
19190
20737
|
stdout(PHASE_LABELS[progress.phase] ?? progress.phase);
|
|
19191
20738
|
}
|
|
19192
20739
|
});
|
|
20740
|
+
if (parsed.fromLinearIssueId) {
|
|
20741
|
+
stdout(`Resolving Linear issue ${parsed.fromLinearIssueId}...`);
|
|
20742
|
+
const seed = await buildSeedFromLinear({ issueId: parsed.fromLinearIssueId }, defaultSeedFromLinearDeps);
|
|
20743
|
+
if (!seed.ok) {
|
|
20744
|
+
stderr(`Linear seed lookup failed: ${seed.error}`);
|
|
20745
|
+
return 1;
|
|
20746
|
+
}
|
|
20747
|
+
stdout(`Linear seed source: ${seed.data.source}${seed.data.branch ? ` branch=${seed.data.branch}` : ""}${seed.data.prUrl ? ` pr=${seed.data.prUrl}` : ""}`);
|
|
20748
|
+
if (!parsed.branchExplicit && seed.data.branch) {
|
|
20749
|
+
parsed.input.branch = seed.data.branch;
|
|
20750
|
+
}
|
|
20751
|
+
if (!parsed.input.branch) {
|
|
20752
|
+
stderr("Linear issue did not resolve to a branch; pass --branch to override.");
|
|
20753
|
+
return 1;
|
|
20754
|
+
}
|
|
20755
|
+
if (seed.data.source !== "none")
|
|
20756
|
+
parsed.input.mode = "existing";
|
|
20757
|
+
if (seed.data.conversationMarkdown) {
|
|
20758
|
+
parsed.input.prompt = parsed.input.prompt ? `${seed.data.conversationMarkdown}
|
|
20759
|
+
|
|
20760
|
+
---
|
|
20761
|
+
|
|
20762
|
+
${parsed.input.prompt}` : seed.data.conversationMarkdown;
|
|
20763
|
+
}
|
|
20764
|
+
}
|
|
19193
20765
|
if (!parsed.input.branch && parsed.input.prompt && runtime2.config.autoName) {
|
|
19194
20766
|
stdout("Generating branch name...");
|
|
19195
20767
|
}
|
|
@@ -19248,23 +20820,13 @@ async function runWorktreeCommand(context, deps2 = {}) {
|
|
|
19248
20820
|
return 0;
|
|
19249
20821
|
}
|
|
19250
20822
|
const api = createApi(`http://localhost:${context.port}`);
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19256
|
-
...parsed.preamble ? { preamble: parsed.preamble } : {}
|
|
19257
|
-
}
|
|
19258
|
-
});
|
|
19259
|
-
} catch (error) {
|
|
19260
|
-
if (error instanceof Error && error.message.startsWith("HTTP")) {
|
|
19261
|
-
throw error;
|
|
20823
|
+
await withServerConnection(context.port, () => api.sendWorktreePrompt({
|
|
20824
|
+
params: { name: parsed.branch },
|
|
20825
|
+
body: {
|
|
20826
|
+
text: parsed.text,
|
|
20827
|
+
...parsed.preamble ? { preamble: parsed.preamble } : {}
|
|
19262
20828
|
}
|
|
19263
|
-
|
|
19264
|
-
throw error;
|
|
19265
|
-
}
|
|
19266
|
-
throw new Error(`Could not connect to webmux server on port ${context.port}. Is it running?`);
|
|
19267
|
-
}
|
|
20829
|
+
}));
|
|
19268
20830
|
stdout(`Sent prompt to ${parsed.branch}`);
|
|
19269
20831
|
return 0;
|
|
19270
20832
|
}
|
|
@@ -19324,10 +20886,12 @@ async function runWorktreeCommand(context, deps2 = {}) {
|
|
|
19324
20886
|
return 1;
|
|
19325
20887
|
}
|
|
19326
20888
|
}
|
|
19327
|
-
var PHASE_LABELS
|
|
20889
|
+
var PHASE_LABELS;
|
|
19328
20890
|
var init_worktree_commands = __esm(() => {
|
|
19329
20891
|
init_dist4();
|
|
19330
20892
|
init_src();
|
|
20893
|
+
init_conversation_export_service();
|
|
20894
|
+
init_shared();
|
|
19331
20895
|
init_fs();
|
|
19332
20896
|
init_tmux();
|
|
19333
20897
|
init_policies();
|
|
@@ -19340,18 +20904,16 @@ var init_worktree_commands = __esm(() => {
|
|
|
19340
20904
|
starting_session: "Starting session",
|
|
19341
20905
|
reconciling: "Reconciling"
|
|
19342
20906
|
};
|
|
19343
|
-
CommandUsageError = class CommandUsageError extends Error {
|
|
19344
|
-
};
|
|
19345
20907
|
});
|
|
19346
20908
|
|
|
19347
20909
|
// bin/src/webmux.ts
|
|
19348
|
-
import { resolve as resolve11, dirname as dirname6, join as
|
|
20910
|
+
import { resolve as resolve11, dirname as dirname6, join as join10 } from "path";
|
|
19349
20911
|
import { existsSync as existsSync5 } from "fs";
|
|
19350
20912
|
import { fileURLToPath } from "url";
|
|
19351
20913
|
// package.json
|
|
19352
20914
|
var package_default = {
|
|
19353
20915
|
name: "webmux",
|
|
19354
|
-
version: "0.
|
|
20916
|
+
version: "0.32.0",
|
|
19355
20917
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
19356
20918
|
type: "module",
|
|
19357
20919
|
repository: {
|
|
@@ -19419,6 +20981,7 @@ Usage:
|
|
|
19419
20981
|
webmux service Manage webmux as a system service
|
|
19420
20982
|
webmux update Update webmux to the latest version
|
|
19421
20983
|
webmux add Create a worktree using the dashboard lifecycle
|
|
20984
|
+
webmux oneshot Run a worktree start-to-finish, streaming logs to stdout
|
|
19422
20985
|
webmux list List worktrees and their status
|
|
19423
20986
|
webmux open Open an existing worktree session
|
|
19424
20987
|
webmux close Close a worktree session without removing it
|
|
@@ -19429,6 +20992,7 @@ Usage:
|
|
|
19429
20992
|
webmux merge Merge a worktree into the main branch and remove it
|
|
19430
20993
|
webmux send Send a prompt to a running worktree agent
|
|
19431
20994
|
webmux prune Remove all worktrees in the current project
|
|
20995
|
+
webmux linear Post a worktree conversation to a Linear issue/team
|
|
19432
20996
|
webmux completion Generate shell completion script (bash, zsh)
|
|
19433
20997
|
|
|
19434
20998
|
Options:
|
|
@@ -19443,7 +21007,7 @@ Environment:
|
|
|
19443
21007
|
`);
|
|
19444
21008
|
}
|
|
19445
21009
|
function isRootCommand(value) {
|
|
19446
|
-
return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "prune" || value === "completion";
|
|
21010
|
+
return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "prune" || value === "linear" || value === "completion";
|
|
19447
21011
|
}
|
|
19448
21012
|
function isServeRootOption(value) {
|
|
19449
21013
|
return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
|
|
@@ -19626,6 +21190,16 @@ async function main(args = process.argv.slice(2)) {
|
|
|
19626
21190
|
}
|
|
19627
21191
|
await loadEnvFile(resolve11(process.cwd(), ".env.local"));
|
|
19628
21192
|
await loadEnvFile(resolve11(process.cwd(), ".env"));
|
|
21193
|
+
if (parsed.command === "oneshot") {
|
|
21194
|
+
const { runOneshotCommand: runOneshotCommand2 } = await Promise.resolve().then(() => (init_oneshot(), exports_oneshot));
|
|
21195
|
+
const exitCode2 = await runOneshotCommand2(parsed.commandArgs, parsed.port);
|
|
21196
|
+
process.exit(exitCode2);
|
|
21197
|
+
}
|
|
21198
|
+
if (parsed.command === "linear") {
|
|
21199
|
+
const { runLinearCommand: runLinearCommand2 } = await Promise.resolve().then(() => (init_linear_commands(), exports_linear_commands));
|
|
21200
|
+
const exitCode2 = await runLinearCommand2(parsed.commandArgs, parsed.port);
|
|
21201
|
+
process.exit(exitCode2);
|
|
21202
|
+
}
|
|
19629
21203
|
if (isWorktreeCommand(parsed.command)) {
|
|
19630
21204
|
const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
|
|
19631
21205
|
const exitCode2 = await runWorktreeCommand2({
|
|
@@ -19674,8 +21248,8 @@ async function main(args = process.argv.slice(2)) {
|
|
|
19674
21248
|
}
|
|
19675
21249
|
process.on("SIGINT", cleanup);
|
|
19676
21250
|
process.on("SIGTERM", cleanup);
|
|
19677
|
-
const backendEntry =
|
|
19678
|
-
const staticDir =
|
|
21251
|
+
const backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
|
|
21252
|
+
const staticDir = join10(PKG_ROOT, "frontend", "dist");
|
|
19679
21253
|
if (!existsSync5(staticDir)) {
|
|
19680
21254
|
console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
|
|
19681
21255
|
process.exit(1);
|