webmux 0.31.2 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/backend/dist/server.js +1180 -109
- package/bin/webmux.js +1777 -230
- package/frontend/dist/assets/{DiffDialog-DXkWdnXl.js → DiffDialog-CtwnOqjo.js} +1 -1
- package/frontend/dist/assets/index-CvURkZrd.css +1 -0
- package/frontend/dist/assets/index-EqF9CRFa.js +34 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-DLB0OmuO.js +0 -33
- package/frontend/dist/assets/index-HGkEqxw6.css +0 -1
package/bin/webmux.js
CHANGED
|
@@ -462,6 +462,7 @@ _webmux() {
|
|
|
462
462
|
'service:Manage webmux as a system service'
|
|
463
463
|
'update:Update webmux to the latest version'
|
|
464
464
|
'add:Create a worktree'
|
|
465
|
+
'oneshot:Run a worktree start-to-finish, streaming logs to stdout'
|
|
465
466
|
'list:List worktrees and their status'
|
|
466
467
|
'open:Open an existing worktree session'
|
|
467
468
|
'close:Close a worktree session'
|
|
@@ -472,6 +473,7 @@ _webmux() {
|
|
|
472
473
|
'merge:Merge a worktree into main'
|
|
473
474
|
'send:Send a prompt to a running worktree agent'
|
|
474
475
|
'prune:Remove all worktrees in the current project'
|
|
476
|
+
'linear:Post a worktree conversation to a Linear issue/team'
|
|
475
477
|
'completion:Generate shell completion script'
|
|
476
478
|
)
|
|
477
479
|
|
|
@@ -490,6 +492,19 @@ _webmux() {
|
|
|
490
492
|
fi
|
|
491
493
|
fi
|
|
492
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
|
+
;;
|
|
493
508
|
completion)
|
|
494
509
|
if (( CURRENT == 3 )); then
|
|
495
510
|
local -a shells
|
|
@@ -519,7 +534,7 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
|
|
|
519
534
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
520
535
|
|
|
521
536
|
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
522
|
-
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}"))
|
|
523
538
|
return
|
|
524
539
|
fi
|
|
525
540
|
|
|
@@ -531,6 +546,15 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
|
|
|
531
546
|
COMPREPLY=($(compgen -W "\${branches}" -- "\${cur}"))
|
|
532
547
|
fi
|
|
533
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
|
+
;;
|
|
534
558
|
completion)
|
|
535
559
|
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
536
560
|
COMPREPLY=($(compgen -W "bash zsh" -- "\${cur}"))
|
|
@@ -1837,7 +1861,29 @@ function detectProjectName(gitRoot) {
|
|
|
1837
1861
|
}
|
|
1838
1862
|
return basename2(gitRoot);
|
|
1839
1863
|
}
|
|
1840
|
-
|
|
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
|
+
});
|
|
1841
1887
|
|
|
1842
1888
|
// bin/src/init-helpers.ts
|
|
1843
1889
|
import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
|
|
@@ -6970,7 +7016,7 @@ var init_zod = __esm(() => {
|
|
|
6970
7016
|
init_external();
|
|
6971
7017
|
});
|
|
6972
7018
|
|
|
6973
|
-
// 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
|
|
6974
7020
|
var isZodType = (obj) => {
|
|
6975
7021
|
return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
|
|
6976
7022
|
}, isZodObjectStrict = (obj) => {
|
|
@@ -7284,7 +7330,15 @@ var init_index_esm = __esm(() => {
|
|
|
7284
7330
|
});
|
|
7285
7331
|
|
|
7286
7332
|
// packages/api-contract/src/schemas.ts
|
|
7287
|
-
|
|
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;
|
|
7288
7342
|
var init_schemas = __esm(() => {
|
|
7289
7343
|
init_zod();
|
|
7290
7344
|
BooleanLikeSchema = exports_external.union([
|
|
@@ -7305,6 +7359,30 @@ var init_schemas = __esm(() => {
|
|
|
7305
7359
|
BuiltInAgentIdSchema = exports_external.enum(["claude", "codex"]);
|
|
7306
7360
|
AgentIdSchema = exports_external.string().trim().min(1);
|
|
7307
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
|
+
});
|
|
7308
7386
|
AgentCapabilitiesSchema = exports_external.object({
|
|
7309
7387
|
terminal: exports_external.literal(true),
|
|
7310
7388
|
inAppChat: exports_external.boolean(),
|
|
@@ -7361,6 +7439,7 @@ var init_schemas = __esm(() => {
|
|
|
7361
7439
|
BranchListResponseSchema = exports_external.object({
|
|
7362
7440
|
branches: exports_external.array(AvailableBranchSchema)
|
|
7363
7441
|
});
|
|
7442
|
+
WorktreeSourceSchema = exports_external.enum(["ui", "oneshot"]);
|
|
7364
7443
|
CreateWorktreeRequestSchema = exports_external.object({
|
|
7365
7444
|
mode: WorktreeCreateModeSchema.optional(),
|
|
7366
7445
|
branch: exports_external.string().optional(),
|
|
@@ -7371,7 +7450,14 @@ var init_schemas = __esm(() => {
|
|
|
7371
7450
|
prompt: exports_external.string().optional(),
|
|
7372
7451
|
envOverrides: exports_external.record(exports_external.string()).optional(),
|
|
7373
7452
|
createLinearTicket: exports_external.literal(true).optional(),
|
|
7374
|
-
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()
|
|
7375
7461
|
});
|
|
7376
7462
|
CreateWorktreeResponseSchema = exports_external.object({
|
|
7377
7463
|
primaryBranch: exports_external.string(),
|
|
@@ -7517,7 +7603,9 @@ var init_schemas = __esm(() => {
|
|
|
7517
7603
|
services: exports_external.array(ServiceStatusSchema),
|
|
7518
7604
|
prs: exports_external.array(PrEntrySchema),
|
|
7519
7605
|
linearIssue: LinkedLinearIssueSchema.nullable(),
|
|
7520
|
-
creation: WorktreeCreationStateSchema.nullable()
|
|
7606
|
+
creation: WorktreeCreationStateSchema.nullable(),
|
|
7607
|
+
source: WorktreeSourceSchema,
|
|
7608
|
+
oneshot: OneshotConfigSchema.nullable()
|
|
7521
7609
|
});
|
|
7522
7610
|
ProjectSnapshotSchema = exports_external.object({
|
|
7523
7611
|
project: exports_external.object({
|
|
@@ -7566,13 +7654,16 @@ var init_schemas = __esm(() => {
|
|
|
7566
7654
|
});
|
|
7567
7655
|
AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
|
|
7568
7656
|
AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
|
|
7657
|
+
AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
|
|
7569
7658
|
AgentsUiConversationMessageSchema = exports_external.object({
|
|
7570
7659
|
id: exports_external.string(),
|
|
7571
7660
|
turnId: exports_external.string(),
|
|
7572
7661
|
role: AgentsUiConversationMessageRoleSchema,
|
|
7573
7662
|
text: exports_external.string(),
|
|
7574
7663
|
status: AgentsUiConversationMessageStatusSchema,
|
|
7575
|
-
createdAt: exports_external.string().nullable()
|
|
7664
|
+
createdAt: exports_external.string().nullable(),
|
|
7665
|
+
kind: AgentsUiConversationMessageKindSchema.optional(),
|
|
7666
|
+
toolName: exports_external.string().optional()
|
|
7576
7667
|
});
|
|
7577
7668
|
AgentsUiConversationStateSchema = exports_external.object({
|
|
7578
7669
|
provider: WorktreeConversationProviderSchema,
|
|
@@ -7701,6 +7792,8 @@ var init_contract = __esm(() => {
|
|
|
7701
7792
|
openWorktree: "/api/worktrees/:name/open",
|
|
7702
7793
|
closeWorktree: "/api/worktrees/:name/close",
|
|
7703
7794
|
setWorktreeArchived: "/api/worktrees/:name/archive",
|
|
7795
|
+
syncWorktreePrs: "/api/worktrees/:name/sync-prs",
|
|
7796
|
+
postWorktreeToLinear: "/api/worktrees/:name/linear/post",
|
|
7704
7797
|
setWorktreeLabel: "/api/worktrees/:name/label",
|
|
7705
7798
|
sendWorktreePrompt: "/api/worktrees/:name/send",
|
|
7706
7799
|
mergeWorktree: "/api/worktrees/:name/merge",
|
|
@@ -7879,7 +7972,7 @@ var init_contract = __esm(() => {
|
|
|
7879
7972
|
method: "POST",
|
|
7880
7973
|
path: apiPaths.openWorktree,
|
|
7881
7974
|
pathParams: WorktreeNameParamsSchema,
|
|
7882
|
-
body:
|
|
7975
|
+
body: OpenWorktreeRequestSchema,
|
|
7883
7976
|
responses: {
|
|
7884
7977
|
200: OkResponseSchema,
|
|
7885
7978
|
...commonErrorResponses
|
|
@@ -7905,6 +7998,26 @@ var init_contract = __esm(() => {
|
|
|
7905
7998
|
...commonErrorResponses
|
|
7906
7999
|
}
|
|
7907
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
|
+
},
|
|
7908
8021
|
setWorktreeLabel: {
|
|
7909
8022
|
method: "PUT",
|
|
7910
8023
|
path: apiPaths.setWorktreeLabel,
|
|
@@ -7962,128 +8075,1464 @@ var init_contract = __esm(() => {
|
|
|
7962
8075
|
...commonErrorResponses
|
|
7963
8076
|
}
|
|
7964
8077
|
},
|
|
7965
|
-
setAutoRemoveOnMerge: {
|
|
7966
|
-
method: "PUT",
|
|
7967
|
-
path: apiPaths.setAutoRemoveOnMerge,
|
|
7968
|
-
body: ToggleEnabledRequestSchema,
|
|
7969
|
-
responses: {
|
|
7970
|
-
200: EnabledResponseSchema,
|
|
7971
|
-
...commonErrorResponses
|
|
7972
|
-
}
|
|
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);
|
|
7973
9333
|
},
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
body: PullMainRequestSchema,
|
|
7978
|
-
responses: {
|
|
7979
|
-
200: PullMainResponseSchema,
|
|
7980
|
-
...commonErrorResponses
|
|
7981
|
-
}
|
|
9334
|
+
onPrEvent: (line) => {
|
|
9335
|
+
flushStreamingLine(conversationState);
|
|
9336
|
+
stdout(`[${timestamp()}] [event] ${line}`);
|
|
7982
9337
|
},
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
responses: {
|
|
7988
|
-
200: CiLogsResponseSchema,
|
|
7989
|
-
...commonErrorResponses
|
|
7990
|
-
}
|
|
9338
|
+
onAgentDone: (reason) => {
|
|
9339
|
+
flushStreamingLine(conversationState);
|
|
9340
|
+
stdout(`[${timestamp()}] [event] ${reason} \u2014 exiting`);
|
|
9341
|
+
finalize(0);
|
|
7991
9342
|
},
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
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];
|
|
8006
9405
|
});
|
|
8007
9406
|
|
|
8008
|
-
//
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
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
|
+
`);
|
|
8019
9434
|
}
|
|
8020
|
-
function
|
|
8021
|
-
|
|
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 };
|
|
8022
9446
|
}
|
|
8023
|
-
function
|
|
8024
|
-
|
|
8025
|
-
|
|
9447
|
+
function parseLinearTargetArg(raw) {
|
|
9448
|
+
const target = parseLinearTarget(raw);
|
|
9449
|
+
if (target.kind === "team") {
|
|
9450
|
+
return { kind: "team", teamKey: target.teamKey };
|
|
8026
9451
|
}
|
|
8027
|
-
if (
|
|
8028
|
-
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).`);
|
|
8029
9454
|
}
|
|
8030
|
-
|
|
9455
|
+
throw new CommandUsageError(`Invalid Linear team key "${target.raw}". Use a team key like ENG.`);
|
|
8031
9456
|
}
|
|
8032
|
-
function
|
|
8033
|
-
if (
|
|
8034
|
-
|
|
8035
|
-
const parsed = JSON.parse(body);
|
|
8036
|
-
return errorMessageFromResponse(parsed, status2);
|
|
8037
|
-
} catch {
|
|
8038
|
-
return body.trim() || `HTTP ${status2}`;
|
|
8039
|
-
}
|
|
8040
|
-
}
|
|
8041
|
-
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
|
8042
|
-
return body.error;
|
|
9457
|
+
function parseLinearArgs(args) {
|
|
9458
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
9459
|
+
return null;
|
|
8043
9460
|
}
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
const [first, ...rest] = args;
|
|
8048
|
-
if (!first || typeof first !== "object" || !("params" in first) || !first.params || typeof first.params !== "object") {
|
|
8049
|
-
return args;
|
|
9461
|
+
const subcommand = args[0];
|
|
9462
|
+
if (subcommand !== "post") {
|
|
9463
|
+
throw new CommandUsageError(`Unknown linear subcommand: ${subcommand}`);
|
|
8050
9464
|
}
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
if (typeof value === "function") {
|
|
8066
|
-
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;
|
|
8067
9479
|
}
|
|
8068
|
-
if (
|
|
8069
|
-
|
|
9480
|
+
if (arg.startsWith("-")) {
|
|
9481
|
+
throw new CommandUsageError(`Unknown option: ${arg}`);
|
|
8070
9482
|
}
|
|
8071
|
-
|
|
8072
|
-
|
|
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
|
+
};
|
|
8073
9503
|
}
|
|
8074
|
-
function
|
|
8075
|
-
|
|
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
|
+
}
|
|
8076
9532
|
}
|
|
8077
|
-
var
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
});
|
|
8081
|
-
|
|
8082
|
-
// packages/api-contract/src/index.ts
|
|
8083
|
-
var init_src = __esm(() => {
|
|
8084
|
-
init_contract();
|
|
8085
|
-
init_client();
|
|
8086
|
-
init_schemas();
|
|
9533
|
+
var init_linear_commands = __esm(() => {
|
|
9534
|
+
init_src();
|
|
9535
|
+
init_shared();
|
|
8087
9536
|
});
|
|
8088
9537
|
|
|
8089
9538
|
// backend/src/domain/model.ts
|
|
@@ -10140,7 +11589,7 @@ var require_merge = __commonJS((exports) => {
|
|
|
10140
11589
|
|
|
10141
11590
|
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/addPairToJSMap.js
|
|
10142
11591
|
var require_addPairToJSMap = __commonJS((exports) => {
|
|
10143
|
-
var
|
|
11592
|
+
var log2 = require_log();
|
|
10144
11593
|
var merge = require_merge();
|
|
10145
11594
|
var stringify = require_stringify();
|
|
10146
11595
|
var identity = require_identity();
|
|
@@ -10189,7 +11638,7 @@ var require_addPairToJSMap = __commonJS((exports) => {
|
|
|
10189
11638
|
let jsonStr = JSON.stringify(strKey);
|
|
10190
11639
|
if (jsonStr.length > 40)
|
|
10191
11640
|
jsonStr = jsonStr.substring(0, 36) + '..."';
|
|
10192
|
-
|
|
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.`);
|
|
10193
11642
|
ctx.mapKeyWarned = true;
|
|
10194
11643
|
}
|
|
10195
11644
|
return strKey;
|
|
@@ -11387,13 +12836,13 @@ var require_timestamp = __commonJS((exports) => {
|
|
|
11387
12836
|
resolve: (str) => parseSexagesimal(str, false),
|
|
11388
12837
|
stringify: stringifySexagesimal
|
|
11389
12838
|
};
|
|
11390
|
-
var
|
|
12839
|
+
var timestamp2 = {
|
|
11391
12840
|
identify: (value) => value instanceof Date,
|
|
11392
12841
|
default: true,
|
|
11393
12842
|
tag: "tag:yaml.org,2002:timestamp",
|
|
11394
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})?))?" + ")?$"),
|
|
11395
12844
|
resolve(str) {
|
|
11396
|
-
const match = str.match(
|
|
12845
|
+
const match = str.match(timestamp2.test);
|
|
11397
12846
|
if (!match)
|
|
11398
12847
|
throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd");
|
|
11399
12848
|
const [, year, month, day, hour, minute, second] = match.map(Number);
|
|
@@ -11412,7 +12861,7 @@ var require_timestamp = __commonJS((exports) => {
|
|
|
11412
12861
|
};
|
|
11413
12862
|
exports.floatTime = floatTime;
|
|
11414
12863
|
exports.intTime = intTime;
|
|
11415
|
-
exports.timestamp =
|
|
12864
|
+
exports.timestamp = timestamp2;
|
|
11416
12865
|
});
|
|
11417
12866
|
|
|
11418
12867
|
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/schema.js
|
|
@@ -11429,7 +12878,7 @@ var require_schema3 = __commonJS((exports) => {
|
|
|
11429
12878
|
var omap = require_omap();
|
|
11430
12879
|
var pairs = require_pairs();
|
|
11431
12880
|
var set = require_set();
|
|
11432
|
-
var
|
|
12881
|
+
var timestamp2 = require_timestamp();
|
|
11433
12882
|
var schema = [
|
|
11434
12883
|
map.map,
|
|
11435
12884
|
seq.seq,
|
|
@@ -11449,9 +12898,9 @@ var require_schema3 = __commonJS((exports) => {
|
|
|
11449
12898
|
omap.omap,
|
|
11450
12899
|
pairs.pairs,
|
|
11451
12900
|
set.set,
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
12901
|
+
timestamp2.intTime,
|
|
12902
|
+
timestamp2.floatTime,
|
|
12903
|
+
timestamp2.timestamp
|
|
11455
12904
|
];
|
|
11456
12905
|
exports.schema = schema;
|
|
11457
12906
|
});
|
|
@@ -11473,7 +12922,7 @@ var require_tags = __commonJS((exports) => {
|
|
|
11473
12922
|
var pairs = require_pairs();
|
|
11474
12923
|
var schema$2 = require_schema3();
|
|
11475
12924
|
var set = require_set();
|
|
11476
|
-
var
|
|
12925
|
+
var timestamp2 = require_timestamp();
|
|
11477
12926
|
var schemas2 = new Map([
|
|
11478
12927
|
["core", schema.schema],
|
|
11479
12928
|
["failsafe", [map.map, seq.seq, string.string]],
|
|
@@ -11487,11 +12936,11 @@ var require_tags = __commonJS((exports) => {
|
|
|
11487
12936
|
float: float.float,
|
|
11488
12937
|
floatExp: float.floatExp,
|
|
11489
12938
|
floatNaN: float.floatNaN,
|
|
11490
|
-
floatTime:
|
|
12939
|
+
floatTime: timestamp2.floatTime,
|
|
11491
12940
|
int: int.int,
|
|
11492
12941
|
intHex: int.intHex,
|
|
11493
12942
|
intOct: int.intOct,
|
|
11494
|
-
intTime:
|
|
12943
|
+
intTime: timestamp2.intTime,
|
|
11495
12944
|
map: map.map,
|
|
11496
12945
|
merge: merge.merge,
|
|
11497
12946
|
null: _null.nullTag,
|
|
@@ -11499,7 +12948,7 @@ var require_tags = __commonJS((exports) => {
|
|
|
11499
12948
|
pairs: pairs.pairs,
|
|
11500
12949
|
seq: seq.seq,
|
|
11501
12950
|
set: set.set,
|
|
11502
|
-
timestamp:
|
|
12951
|
+
timestamp: timestamp2.timestamp
|
|
11503
12952
|
};
|
|
11504
12953
|
var coreKnownTags = {
|
|
11505
12954
|
"tag:yaml.org,2002:binary": binary.binary,
|
|
@@ -11507,7 +12956,7 @@ var require_tags = __commonJS((exports) => {
|
|
|
11507
12956
|
"tag:yaml.org,2002:omap": omap.omap,
|
|
11508
12957
|
"tag:yaml.org,2002:pairs": pairs.pairs,
|
|
11509
12958
|
"tag:yaml.org,2002:set": set.set,
|
|
11510
|
-
"tag:yaml.org,2002:timestamp":
|
|
12959
|
+
"tag:yaml.org,2002:timestamp": timestamp2.timestamp
|
|
11511
12960
|
};
|
|
11512
12961
|
function getTags(customTags, schemaName, addMergeTag) {
|
|
11513
12962
|
const schemaTags = schemas2.get(schemaName);
|
|
@@ -12775,9 +14224,9 @@ var require_resolve_block_scalar = __commonJS((exports) => {
|
|
|
12775
14224
|
default: {
|
|
12776
14225
|
const message = `Unexpected token in block scalar header: ${token.type}`;
|
|
12777
14226
|
onError(token, "UNEXPECTED_TOKEN", message);
|
|
12778
|
-
const
|
|
12779
|
-
if (
|
|
12780
|
-
length +=
|
|
14227
|
+
const ts2 = token.source;
|
|
14228
|
+
if (ts2 && typeof ts2 === "string")
|
|
14229
|
+
length += ts2.length;
|
|
12781
14230
|
}
|
|
12782
14231
|
}
|
|
12783
14232
|
}
|
|
@@ -13080,9 +14529,9 @@ var require_compose_scalar = __commonJS((exports) => {
|
|
|
13080
14529
|
if (schema.compat) {
|
|
13081
14530
|
const compat = schema.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema[identity.SCALAR];
|
|
13082
14531
|
if (tag.tag !== compat.tag) {
|
|
13083
|
-
const
|
|
14532
|
+
const ts2 = directives.tagString(tag.tag);
|
|
13084
14533
|
const cs = directives.tagString(compat.tag);
|
|
13085
|
-
const msg = `Value may be parsed as either ${
|
|
14534
|
+
const msg = `Value may be parsed as either ${ts2} or ${cs}`;
|
|
13086
14535
|
onError(token, "TAG_RESOLVE_FAILED", msg, true);
|
|
13087
14536
|
}
|
|
13088
14537
|
}
|
|
@@ -15346,7 +16795,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
15346
16795
|
var composer = require_composer();
|
|
15347
16796
|
var Document = require_Document();
|
|
15348
16797
|
var errors2 = require_errors();
|
|
15349
|
-
var
|
|
16798
|
+
var log2 = require_log();
|
|
15350
16799
|
var identity = require_identity();
|
|
15351
16800
|
var lineCounter = require_line_counter();
|
|
15352
16801
|
var parser = require_parser();
|
|
@@ -15398,7 +16847,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
15398
16847
|
const doc = parseDocument(src, options);
|
|
15399
16848
|
if (!doc)
|
|
15400
16849
|
return null;
|
|
15401
|
-
doc.warnings.forEach((warning) =>
|
|
16850
|
+
doc.warnings.forEach((warning) => log2.warn(doc.options.logLevel, warning));
|
|
15402
16851
|
if (doc.errors.length > 0) {
|
|
15403
16852
|
if (doc.options.logLevel !== "silent")
|
|
15404
16853
|
throw doc.errors[0];
|
|
@@ -15487,6 +16936,15 @@ var init_dist5 = __esm(() => {
|
|
|
15487
16936
|
// backend/src/adapters/config.ts
|
|
15488
16937
|
import { readFileSync as readFileSync3 } from "fs";
|
|
15489
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
|
+
}
|
|
15490
16948
|
function clonePanes(panes) {
|
|
15491
16949
|
return panes.map((pane) => ({ ...pane }));
|
|
15492
16950
|
}
|
|
@@ -15663,6 +17121,12 @@ function parseLifecycleHooks(raw) {
|
|
|
15663
17121
|
}
|
|
15664
17122
|
return hooks;
|
|
15665
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
|
+
}
|
|
15666
17130
|
function parseAutoName(raw) {
|
|
15667
17131
|
if (!isRecord4(raw))
|
|
15668
17132
|
return null;
|
|
@@ -15735,7 +17199,8 @@ function parseProjectConfig(parsed) {
|
|
|
15735
17199
|
}
|
|
15736
17200
|
},
|
|
15737
17201
|
lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
|
|
15738
|
-
autoName: parseAutoName(parsed.auto_name)
|
|
17202
|
+
autoName: parseAutoName(parsed.auto_name),
|
|
17203
|
+
oneshot: parseOneshot(parsed.oneshot)
|
|
15739
17204
|
};
|
|
15740
17205
|
}
|
|
15741
17206
|
function defaultConfig() {
|
|
@@ -15902,7 +17367,8 @@ var init_config = __esm(() => {
|
|
|
15902
17367
|
linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
|
|
15903
17368
|
},
|
|
15904
17369
|
lifecycleHooks: {},
|
|
15905
|
-
autoName: null
|
|
17370
|
+
autoName: null,
|
|
17371
|
+
oneshot: { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() }
|
|
15906
17372
|
};
|
|
15907
17373
|
});
|
|
15908
17374
|
|
|
@@ -15929,30 +17395,6 @@ var init_control_token = __esm(() => {
|
|
|
15929
17395
|
CONTROL_TOKEN_PATH = `${Bun.env.HOME ?? "/root"}/.config/webmux/control-token`;
|
|
15930
17396
|
});
|
|
15931
17397
|
|
|
15932
|
-
// backend/src/lib/log.ts
|
|
15933
|
-
function ts() {
|
|
15934
|
-
return new Date().toISOString().slice(11, 23);
|
|
15935
|
-
}
|
|
15936
|
-
var DEBUG, log;
|
|
15937
|
-
var init_log = __esm(() => {
|
|
15938
|
-
DEBUG = Bun.env.WEBMUX_DEBUG === "1";
|
|
15939
|
-
log = {
|
|
15940
|
-
info(msg) {
|
|
15941
|
-
console.log(`[${ts()}] ${msg}`);
|
|
15942
|
-
},
|
|
15943
|
-
debug(msg) {
|
|
15944
|
-
if (DEBUG)
|
|
15945
|
-
console.log(`[${ts()}] ${msg}`);
|
|
15946
|
-
},
|
|
15947
|
-
warn(msg) {
|
|
15948
|
-
console.warn(`[${ts()}] ${msg}`);
|
|
15949
|
-
},
|
|
15950
|
-
error(msg, err) {
|
|
15951
|
-
err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
|
|
15952
|
-
}
|
|
15953
|
-
};
|
|
15954
|
-
});
|
|
15955
|
-
|
|
15956
17398
|
// backend/src/adapters/docker.ts
|
|
15957
17399
|
import { stat } from "fs/promises";
|
|
15958
17400
|
async function pathExists(p2) {
|
|
@@ -17046,23 +18488,22 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
|
17046
18488
|
return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
|
|
17047
18489
|
}
|
|
17048
18490
|
function buildBuiltInAgentInvocation(input) {
|
|
18491
|
+
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
17049
18492
|
if (input.agent === "codex") {
|
|
17050
18493
|
const hooksFlag = " --enable codex_hooks";
|
|
17051
18494
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
17052
18495
|
if (input.launchMode === "resume") {
|
|
17053
|
-
return `codex${hooksFlag}${yoloFlag2} resume --last`;
|
|
18496
|
+
return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
|
|
17054
18497
|
}
|
|
17055
|
-
const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
17056
18498
|
if (input.systemPrompt) {
|
|
17057
|
-
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${
|
|
18499
|
+
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
|
|
17058
18500
|
}
|
|
17059
|
-
return `codex${hooksFlag}${yoloFlag2}${
|
|
18501
|
+
return `codex${hooksFlag}${yoloFlag2}${promptSuffix}`;
|
|
17060
18502
|
}
|
|
17061
18503
|
const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
|
|
17062
18504
|
if (input.launchMode === "resume") {
|
|
17063
|
-
return `claude${yoloFlag} --continue`;
|
|
18505
|
+
return `claude${yoloFlag} --continue${promptSuffix}`;
|
|
17064
18506
|
}
|
|
17065
|
-
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
17066
18507
|
if (input.systemPrompt) {
|
|
17067
18508
|
return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
|
|
17068
18509
|
}
|
|
@@ -17380,7 +18821,9 @@ async function initializeManagedWorktree(opts) {
|
|
|
17380
18821
|
agent: opts.agent,
|
|
17381
18822
|
runtime: opts.runtime,
|
|
17382
18823
|
startupEnvValues: { ...opts.startupEnvValues ?? {} },
|
|
17383
|
-
allocatedPorts: { ...opts.allocatedPorts ?? {} }
|
|
18824
|
+
allocatedPorts: { ...opts.allocatedPorts ?? {} },
|
|
18825
|
+
...opts.source ? { source: opts.source } : {},
|
|
18826
|
+
...opts.oneshot ? { oneshot: opts.oneshot } : {}
|
|
17384
18827
|
};
|
|
17385
18828
|
const paths = await ensureWorktreeStorageDirs(opts.gitDir);
|
|
17386
18829
|
await writeWorktreeMeta(opts.gitDir, meta);
|
|
@@ -17433,7 +18876,9 @@ async function createManagedWorktree(opts, deps2 = {}) {
|
|
|
17433
18876
|
controlUrl: opts.controlUrl,
|
|
17434
18877
|
controlToken: opts.controlToken,
|
|
17435
18878
|
now: opts.now,
|
|
17436
|
-
worktreeId: opts.worktreeId
|
|
18879
|
+
worktreeId: opts.worktreeId,
|
|
18880
|
+
...opts.source ? { source: opts.source } : {},
|
|
18881
|
+
...opts.oneshot ? { oneshot: opts.oneshot } : {}
|
|
17437
18882
|
});
|
|
17438
18883
|
if (deps2.tmux) {
|
|
17439
18884
|
sessionLayoutPlan = sessionLayoutPlan ?? opts.sessionLayoutPlanBuilder?.(initialized);
|
|
@@ -17573,10 +19018,15 @@ class LifecycleService {
|
|
|
17573
19018
|
agent: agent.id
|
|
17574
19019
|
});
|
|
17575
19020
|
}
|
|
17576
|
-
async openWorktree(branch) {
|
|
19021
|
+
async openWorktree(branch, options = {}) {
|
|
17577
19022
|
try {
|
|
17578
19023
|
const resolved = await this.resolveExistingWorktree(branch);
|
|
17579
|
-
|
|
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
|
+
}
|
|
17580
19030
|
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
17581
19031
|
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
17582
19032
|
const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
|
|
@@ -17591,7 +19041,8 @@ class LifecycleService {
|
|
|
17591
19041
|
agent,
|
|
17592
19042
|
initialized,
|
|
17593
19043
|
worktreePath: resolved.entry.path,
|
|
17594
|
-
launchMode
|
|
19044
|
+
launchMode,
|
|
19045
|
+
followUpPrompt: options.prompt
|
|
17595
19046
|
});
|
|
17596
19047
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
17597
19048
|
return {
|
|
@@ -17602,6 +19053,20 @@ class LifecycleService {
|
|
|
17602
19053
|
throw this.wrapOperationError(error);
|
|
17603
19054
|
}
|
|
17604
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
|
+
}
|
|
17605
19070
|
async closeWorktree(branch) {
|
|
17606
19071
|
try {
|
|
17607
19072
|
await this.resolveExistingWorktree(branch);
|
|
@@ -17888,8 +19353,10 @@ class LifecycleService {
|
|
|
17888
19353
|
agent: input.agent,
|
|
17889
19354
|
initialized: input.initialized,
|
|
17890
19355
|
worktreePath: input.worktreePath,
|
|
17891
|
-
|
|
19356
|
+
creationPrompt: input.creationPrompt,
|
|
19357
|
+
followUpPrompt: input.followUpPrompt,
|
|
17892
19358
|
launchMode: input.launchMode,
|
|
19359
|
+
source: input.source,
|
|
17893
19360
|
containerName: containerName2
|
|
17894
19361
|
}));
|
|
17895
19362
|
return;
|
|
@@ -17901,12 +19368,19 @@ class LifecycleService {
|
|
|
17901
19368
|
agent: input.agent,
|
|
17902
19369
|
initialized: input.initialized,
|
|
17903
19370
|
worktreePath: input.worktreePath,
|
|
17904
|
-
|
|
17905
|
-
|
|
19371
|
+
creationPrompt: input.creationPrompt,
|
|
19372
|
+
followUpPrompt: input.followUpPrompt,
|
|
19373
|
+
launchMode: input.launchMode,
|
|
19374
|
+
source: input.source
|
|
17906
19375
|
}));
|
|
17907
19376
|
}
|
|
17908
19377
|
buildSessionLayout(input) {
|
|
17909
|
-
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;
|
|
17910
19384
|
const containerName2 = input.containerName;
|
|
17911
19385
|
return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
|
|
17912
19386
|
repoRoot: this.deps.projectRoot,
|
|
@@ -17921,7 +19395,7 @@ class LifecycleService {
|
|
|
17921
19395
|
profileName: input.profileName,
|
|
17922
19396
|
yolo: input.profile.yolo === true,
|
|
17923
19397
|
systemPrompt,
|
|
17924
|
-
prompt
|
|
19398
|
+
prompt,
|
|
17925
19399
|
launchMode: input.launchMode
|
|
17926
19400
|
}),
|
|
17927
19401
|
shell: buildDockerShellCommand(containerName2, input.worktreePath, input.initialized.paths.runtimeEnvPath)
|
|
@@ -17935,7 +19409,7 @@ class LifecycleService {
|
|
|
17935
19409
|
profileName: input.profileName,
|
|
17936
19410
|
yolo: input.profile.yolo === true,
|
|
17937
19411
|
systemPrompt,
|
|
17938
|
-
prompt
|
|
19412
|
+
prompt,
|
|
17939
19413
|
launchMode: input.launchMode
|
|
17940
19414
|
}),
|
|
17941
19415
|
shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
|
|
@@ -18059,12 +19533,14 @@ class LifecycleService {
|
|
|
18059
19533
|
const { profileName, profile } = this.resolveProfile(input.profile);
|
|
18060
19534
|
const agent = this.resolveAgentDefinition(input.agent);
|
|
18061
19535
|
const worktreePath = this.resolveWorktreePath(input.branch);
|
|
19536
|
+
const source = input.source ?? "ui";
|
|
18062
19537
|
const createProgressBase = {
|
|
18063
19538
|
branch: input.branch,
|
|
18064
19539
|
...baseBranch ? { baseBranch } : {},
|
|
18065
19540
|
path: worktreePath,
|
|
18066
19541
|
profile: profileName,
|
|
18067
|
-
agent: input.agent
|
|
19542
|
+
agent: input.agent,
|
|
19543
|
+
source
|
|
18068
19544
|
};
|
|
18069
19545
|
const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
|
|
18070
19546
|
let initialized = null;
|
|
@@ -18089,7 +19565,9 @@ class LifecycleService {
|
|
|
18089
19565
|
runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
|
|
18090
19566
|
controlUrl: this.controlUrl(profile.runtime),
|
|
18091
19567
|
controlToken: await this.deps.getControlToken(),
|
|
18092
|
-
deleteBranchOnRollback
|
|
19568
|
+
deleteBranchOnRollback,
|
|
19569
|
+
source,
|
|
19570
|
+
...input.oneshot ? { oneshot: input.oneshot } : {}
|
|
18093
19571
|
}, {
|
|
18094
19572
|
git: this.deps.git
|
|
18095
19573
|
});
|
|
@@ -18127,8 +19605,9 @@ class LifecycleService {
|
|
|
18127
19605
|
agent,
|
|
18128
19606
|
initialized,
|
|
18129
19607
|
worktreePath,
|
|
18130
|
-
|
|
18131
|
-
launchMode: "fresh"
|
|
19608
|
+
creationPrompt: input.prompt,
|
|
19609
|
+
launchMode: "fresh",
|
|
19610
|
+
source
|
|
18132
19611
|
});
|
|
18133
19612
|
await this.reportCreateProgress({
|
|
18134
19613
|
...createProgressBase,
|
|
@@ -18290,6 +19769,8 @@ function makeDefaultState(input) {
|
|
|
18290
19769
|
path: input.path,
|
|
18291
19770
|
profile: input.profile ?? null,
|
|
18292
19771
|
agentName: input.agentName ?? null,
|
|
19772
|
+
source: input.source ?? "ui",
|
|
19773
|
+
oneshot: input.oneshot ?? null,
|
|
18293
19774
|
git: {
|
|
18294
19775
|
exists: true,
|
|
18295
19776
|
branch: input.branch,
|
|
@@ -18339,6 +19820,10 @@ class ProjectRuntime {
|
|
|
18339
19820
|
existing.agentName = input.agentName ?? existing.agentName;
|
|
18340
19821
|
if (input.runtime)
|
|
18341
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;
|
|
18342
19827
|
existing.git.exists = true;
|
|
18343
19828
|
existing.git.branch = input.branch;
|
|
18344
19829
|
existing.session.windowName = buildWorktreeWindowName(input.branch);
|
|
@@ -18349,6 +19834,11 @@ class ProjectRuntime {
|
|
|
18349
19834
|
this.worktreeIdsByBranch.set(input.branch, input.worktreeId);
|
|
18350
19835
|
return created;
|
|
18351
19836
|
}
|
|
19837
|
+
setOneshot(worktreeId, oneshot) {
|
|
19838
|
+
const state = this.requireWorktree(worktreeId);
|
|
19839
|
+
state.oneshot = oneshot;
|
|
19840
|
+
return state;
|
|
19841
|
+
}
|
|
18352
19842
|
removeWorktree(worktreeId) {
|
|
18353
19843
|
const state = this.worktrees.get(worktreeId);
|
|
18354
19844
|
if (!state)
|
|
@@ -18394,36 +19884,36 @@ class ProjectRuntime {
|
|
|
18394
19884
|
if (event.branch !== state.branch) {
|
|
18395
19885
|
this.applyBranchChange(state, event.branch);
|
|
18396
19886
|
}
|
|
18397
|
-
const
|
|
19887
|
+
const timestamp2 = isoNow(now);
|
|
18398
19888
|
switch (event.type) {
|
|
18399
19889
|
case "agent_stopped":
|
|
18400
19890
|
state.agent.lifecycle = "stopped";
|
|
18401
|
-
state.agent.lastEventAt =
|
|
19891
|
+
state.agent.lastEventAt = timestamp2;
|
|
18402
19892
|
break;
|
|
18403
19893
|
case "agent_status_changed":
|
|
18404
|
-
this.applyStatusChanged(state, event,
|
|
19894
|
+
this.applyStatusChanged(state, event, timestamp2);
|
|
18405
19895
|
break;
|
|
18406
19896
|
case "runtime_error":
|
|
18407
|
-
this.applyRuntimeError(state, event,
|
|
19897
|
+
this.applyRuntimeError(state, event, timestamp2);
|
|
18408
19898
|
break;
|
|
18409
19899
|
case "pr_opened":
|
|
18410
|
-
state.agent.lastEventAt =
|
|
19900
|
+
state.agent.lastEventAt = timestamp2;
|
|
18411
19901
|
break;
|
|
18412
19902
|
}
|
|
18413
19903
|
return state;
|
|
18414
19904
|
}
|
|
18415
|
-
applyStatusChanged(state, event,
|
|
19905
|
+
applyStatusChanged(state, event, timestamp2) {
|
|
18416
19906
|
state.agent.lifecycle = event.lifecycle;
|
|
18417
|
-
state.agent.lastEventAt =
|
|
19907
|
+
state.agent.lastEventAt = timestamp2;
|
|
18418
19908
|
if (state.agent.lastStartedAt === null && event.lifecycle === "running") {
|
|
18419
|
-
state.agent.lastStartedAt =
|
|
19909
|
+
state.agent.lastStartedAt = timestamp2;
|
|
18420
19910
|
}
|
|
18421
19911
|
state.agent.lastError = null;
|
|
18422
19912
|
}
|
|
18423
|
-
applyRuntimeError(state, event,
|
|
19913
|
+
applyRuntimeError(state, event, timestamp2) {
|
|
18424
19914
|
state.agent.lifecycle = "error";
|
|
18425
19915
|
state.agent.lastError = event.message;
|
|
18426
|
-
state.agent.lastEventAt =
|
|
19916
|
+
state.agent.lastEventAt = timestamp2;
|
|
18427
19917
|
}
|
|
18428
19918
|
applyBranchChange(state, branch) {
|
|
18429
19919
|
this.reindexBranch(state.branch, branch, state.worktreeId);
|
|
@@ -18560,6 +20050,8 @@ class ReconciliationService {
|
|
|
18560
20050
|
profile: meta?.profile ?? null,
|
|
18561
20051
|
agentName: meta?.agent ?? null,
|
|
18562
20052
|
runtime: meta?.runtime ?? "host",
|
|
20053
|
+
source: meta?.source ?? "ui",
|
|
20054
|
+
oneshot: meta?.oneshot ?? null,
|
|
18563
20055
|
git: {
|
|
18564
20056
|
dirty: gitStatus.dirty,
|
|
18565
20057
|
aheadCount: gitStatus.aheadCount,
|
|
@@ -18592,7 +20084,9 @@ class ReconciliationService {
|
|
|
18592
20084
|
path: state.path,
|
|
18593
20085
|
profile: state.profile,
|
|
18594
20086
|
agentName: state.agentName,
|
|
18595
|
-
runtime: state.runtime
|
|
20087
|
+
runtime: state.runtime,
|
|
20088
|
+
source: state.source,
|
|
20089
|
+
oneshot: state.oneshot
|
|
18596
20090
|
});
|
|
18597
20091
|
this.deps.runtime.setGitState(state.worktreeId, {
|
|
18598
20092
|
exists: true,
|
|
@@ -18632,7 +20126,8 @@ class WorktreeCreationTracker {
|
|
|
18632
20126
|
path: progress.path,
|
|
18633
20127
|
profile: progress.profile,
|
|
18634
20128
|
agentName: progress.agent,
|
|
18635
|
-
phase: progress.phase
|
|
20129
|
+
phase: progress.phase,
|
|
20130
|
+
source: progress.source
|
|
18636
20131
|
};
|
|
18637
20132
|
this.worktrees.set(progress.branch, next);
|
|
18638
20133
|
}
|
|
@@ -18738,7 +20233,7 @@ function getWorktreeCommandUsage(command) {
|
|
|
18738
20233
|
case "add":
|
|
18739
20234
|
return [
|
|
18740
20235
|
"Usage:",
|
|
18741
|
-
" 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>]",
|
|
18742
20237
|
"",
|
|
18743
20238
|
"Options:",
|
|
18744
20239
|
" --existing Use an existing local or remote branch instead of creating a new one",
|
|
@@ -18748,6 +20243,8 @@ function getWorktreeCommandUsage(command) {
|
|
|
18748
20243
|
" --prompt <text> Initial agent prompt",
|
|
18749
20244
|
" --env KEY=VALUE Runtime env override (repeatable)",
|
|
18750
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",
|
|
18751
20248
|
" --help Show this help message"
|
|
18752
20249
|
].join(`
|
|
18753
20250
|
`);
|
|
@@ -18809,7 +20306,7 @@ function getWorktreeCommandUsage(command) {
|
|
|
18809
20306
|
webmux prune`;
|
|
18810
20307
|
}
|
|
18811
20308
|
}
|
|
18812
|
-
function
|
|
20309
|
+
function readOptionValue3(args, index, flag) {
|
|
18813
20310
|
const arg = args[index];
|
|
18814
20311
|
if (!arg) {
|
|
18815
20312
|
throw new CommandUsageError(`${flag} requires a value`);
|
|
@@ -18842,6 +20339,8 @@ function parseAddCommandArgs(args) {
|
|
|
18842
20339
|
const envOverrides = {};
|
|
18843
20340
|
const selectedAgents = [];
|
|
18844
20341
|
let detach = false;
|
|
20342
|
+
let fromLinearIssueId = null;
|
|
20343
|
+
let branchExplicit = false;
|
|
18845
20344
|
for (let index = 0;index < args.length; index++) {
|
|
18846
20345
|
const arg = args[index];
|
|
18847
20346
|
if (!arg)
|
|
@@ -18858,31 +20357,31 @@ function parseAddCommandArgs(args) {
|
|
|
18858
20357
|
continue;
|
|
18859
20358
|
}
|
|
18860
20359
|
if (arg === "--profile" || arg.startsWith("--profile=")) {
|
|
18861
|
-
const { value, nextIndex } =
|
|
20360
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--profile");
|
|
18862
20361
|
input.profile = value;
|
|
18863
20362
|
index = nextIndex;
|
|
18864
20363
|
continue;
|
|
18865
20364
|
}
|
|
18866
20365
|
if (arg === "--base" || arg.startsWith("--base=")) {
|
|
18867
|
-
const { value, nextIndex } =
|
|
20366
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--base");
|
|
18868
20367
|
input.baseBranch = value;
|
|
18869
20368
|
index = nextIndex;
|
|
18870
20369
|
continue;
|
|
18871
20370
|
}
|
|
18872
20371
|
if (arg === "--agent" || arg.startsWith("--agent=")) {
|
|
18873
|
-
const { value, nextIndex } =
|
|
20372
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--agent");
|
|
18874
20373
|
selectedAgents.push(parseAgent(value));
|
|
18875
20374
|
index = nextIndex;
|
|
18876
20375
|
continue;
|
|
18877
20376
|
}
|
|
18878
20377
|
if (arg === "--prompt" || arg.startsWith("--prompt=")) {
|
|
18879
|
-
const { value, nextIndex } =
|
|
20378
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--prompt");
|
|
18880
20379
|
input.prompt = value;
|
|
18881
20380
|
index = nextIndex;
|
|
18882
20381
|
continue;
|
|
18883
20382
|
}
|
|
18884
20383
|
if (arg === "--env" || arg.startsWith("--env=")) {
|
|
18885
|
-
const { value, nextIndex } =
|
|
20384
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--env");
|
|
18886
20385
|
const separatorIndex = value.indexOf("=");
|
|
18887
20386
|
if (separatorIndex <= 0) {
|
|
18888
20387
|
throw new CommandUsageError("--env must use KEY=VALUE");
|
|
@@ -18891,6 +20390,26 @@ function parseAddCommandArgs(args) {
|
|
|
18891
20390
|
index = nextIndex;
|
|
18892
20391
|
continue;
|
|
18893
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
|
+
}
|
|
18894
20413
|
if (arg.startsWith("-")) {
|
|
18895
20414
|
throw new CommandUsageError(`Unknown option: ${arg}`);
|
|
18896
20415
|
}
|
|
@@ -18898,6 +20417,7 @@ function parseAddCommandArgs(args) {
|
|
|
18898
20417
|
throw new CommandUsageError(`Unexpected argument: ${arg}`);
|
|
18899
20418
|
}
|
|
18900
20419
|
input.branch = arg;
|
|
20420
|
+
branchExplicit = true;
|
|
18901
20421
|
}
|
|
18902
20422
|
if (selectedAgents.length > 0) {
|
|
18903
20423
|
input.agents = selectedAgents;
|
|
@@ -18905,7 +20425,7 @@ function parseAddCommandArgs(args) {
|
|
|
18905
20425
|
if (Object.keys(envOverrides).length > 0) {
|
|
18906
20426
|
input.envOverrides = envOverrides;
|
|
18907
20427
|
}
|
|
18908
|
-
return { input, detach };
|
|
20428
|
+
return { input, detach, fromLinearIssueId, branchExplicit };
|
|
18909
20429
|
}
|
|
18910
20430
|
function parseBranchCommandArgs(args) {
|
|
18911
20431
|
let branch = null;
|
|
@@ -18949,7 +20469,7 @@ function parseLabelCommandArgs(args) {
|
|
|
18949
20469
|
if (optionLabel !== null) {
|
|
18950
20470
|
throw new CommandUsageError("Cannot use --label more than once");
|
|
18951
20471
|
}
|
|
18952
|
-
const { value, nextIndex } =
|
|
20472
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--label");
|
|
18953
20473
|
optionLabel = value;
|
|
18954
20474
|
index = nextIndex;
|
|
18955
20475
|
continue;
|
|
@@ -19001,13 +20521,13 @@ function parseSendCommandArgs(args) {
|
|
|
19001
20521
|
if (arg === "--prompt" || arg.startsWith("--prompt=")) {
|
|
19002
20522
|
if (text)
|
|
19003
20523
|
throw new CommandUsageError("Cannot use --prompt with a positional prompt argument");
|
|
19004
|
-
const { value, nextIndex } =
|
|
20524
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--prompt");
|
|
19005
20525
|
text = value;
|
|
19006
20526
|
index = nextIndex;
|
|
19007
20527
|
continue;
|
|
19008
20528
|
}
|
|
19009
20529
|
if (arg === "--preamble" || arg.startsWith("--preamble=")) {
|
|
19010
|
-
const { value, nextIndex } =
|
|
20530
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--preamble");
|
|
19011
20531
|
preamble = value;
|
|
19012
20532
|
index = nextIndex;
|
|
19013
20533
|
continue;
|
|
@@ -19073,7 +20593,7 @@ function parseListCommandArgs(args) {
|
|
|
19073
20593
|
continue;
|
|
19074
20594
|
}
|
|
19075
20595
|
if (arg === "--search" || arg.startsWith("--search=")) {
|
|
19076
|
-
const { value, nextIndex } =
|
|
20596
|
+
const { value, nextIndex } = readOptionValue3(args, index, "--search");
|
|
19077
20597
|
search = value;
|
|
19078
20598
|
index = nextIndex;
|
|
19079
20599
|
continue;
|
|
@@ -19217,6 +20737,31 @@ async function runWorktreeCommand(context, deps2 = {}) {
|
|
|
19217
20737
|
stdout(PHASE_LABELS[progress.phase] ?? progress.phase);
|
|
19218
20738
|
}
|
|
19219
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
|
+
}
|
|
19220
20765
|
if (!parsed.input.branch && parsed.input.prompt && runtime2.config.autoName) {
|
|
19221
20766
|
stdout("Generating branch name...");
|
|
19222
20767
|
}
|
|
@@ -19275,23 +20820,13 @@ async function runWorktreeCommand(context, deps2 = {}) {
|
|
|
19275
20820
|
return 0;
|
|
19276
20821
|
}
|
|
19277
20822
|
const api = createApi(`http://localhost:${context.port}`);
|
|
19278
|
-
|
|
19279
|
-
|
|
19280
|
-
|
|
19281
|
-
|
|
19282
|
-
|
|
19283
|
-
...parsed.preamble ? { preamble: parsed.preamble } : {}
|
|
19284
|
-
}
|
|
19285
|
-
});
|
|
19286
|
-
} catch (error) {
|
|
19287
|
-
if (error instanceof Error && error.message.startsWith("HTTP")) {
|
|
19288
|
-
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 } : {}
|
|
19289
20828
|
}
|
|
19290
|
-
|
|
19291
|
-
throw error;
|
|
19292
|
-
}
|
|
19293
|
-
throw new Error(`Could not connect to webmux server on port ${context.port}. Is it running?`);
|
|
19294
|
-
}
|
|
20829
|
+
}));
|
|
19295
20830
|
stdout(`Sent prompt to ${parsed.branch}`);
|
|
19296
20831
|
return 0;
|
|
19297
20832
|
}
|
|
@@ -19351,10 +20886,12 @@ async function runWorktreeCommand(context, deps2 = {}) {
|
|
|
19351
20886
|
return 1;
|
|
19352
20887
|
}
|
|
19353
20888
|
}
|
|
19354
|
-
var PHASE_LABELS
|
|
20889
|
+
var PHASE_LABELS;
|
|
19355
20890
|
var init_worktree_commands = __esm(() => {
|
|
19356
20891
|
init_dist4();
|
|
19357
20892
|
init_src();
|
|
20893
|
+
init_conversation_export_service();
|
|
20894
|
+
init_shared();
|
|
19358
20895
|
init_fs();
|
|
19359
20896
|
init_tmux();
|
|
19360
20897
|
init_policies();
|
|
@@ -19367,18 +20904,16 @@ var init_worktree_commands = __esm(() => {
|
|
|
19367
20904
|
starting_session: "Starting session",
|
|
19368
20905
|
reconciling: "Reconciling"
|
|
19369
20906
|
};
|
|
19370
|
-
CommandUsageError = class CommandUsageError extends Error {
|
|
19371
|
-
};
|
|
19372
20907
|
});
|
|
19373
20908
|
|
|
19374
20909
|
// bin/src/webmux.ts
|
|
19375
|
-
import { resolve as resolve11, dirname as dirname6, join as
|
|
20910
|
+
import { resolve as resolve11, dirname as dirname6, join as join10 } from "path";
|
|
19376
20911
|
import { existsSync as existsSync5 } from "fs";
|
|
19377
20912
|
import { fileURLToPath } from "url";
|
|
19378
20913
|
// package.json
|
|
19379
20914
|
var package_default = {
|
|
19380
20915
|
name: "webmux",
|
|
19381
|
-
version: "0.
|
|
20916
|
+
version: "0.32.0",
|
|
19382
20917
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
19383
20918
|
type: "module",
|
|
19384
20919
|
repository: {
|
|
@@ -19446,6 +20981,7 @@ Usage:
|
|
|
19446
20981
|
webmux service Manage webmux as a system service
|
|
19447
20982
|
webmux update Update webmux to the latest version
|
|
19448
20983
|
webmux add Create a worktree using the dashboard lifecycle
|
|
20984
|
+
webmux oneshot Run a worktree start-to-finish, streaming logs to stdout
|
|
19449
20985
|
webmux list List worktrees and their status
|
|
19450
20986
|
webmux open Open an existing worktree session
|
|
19451
20987
|
webmux close Close a worktree session without removing it
|
|
@@ -19456,6 +20992,7 @@ Usage:
|
|
|
19456
20992
|
webmux merge Merge a worktree into the main branch and remove it
|
|
19457
20993
|
webmux send Send a prompt to a running worktree agent
|
|
19458
20994
|
webmux prune Remove all worktrees in the current project
|
|
20995
|
+
webmux linear Post a worktree conversation to a Linear issue/team
|
|
19459
20996
|
webmux completion Generate shell completion script (bash, zsh)
|
|
19460
20997
|
|
|
19461
20998
|
Options:
|
|
@@ -19470,7 +21007,7 @@ Environment:
|
|
|
19470
21007
|
`);
|
|
19471
21008
|
}
|
|
19472
21009
|
function isRootCommand(value) {
|
|
19473
|
-
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";
|
|
19474
21011
|
}
|
|
19475
21012
|
function isServeRootOption(value) {
|
|
19476
21013
|
return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
|
|
@@ -19653,6 +21190,16 @@ async function main(args = process.argv.slice(2)) {
|
|
|
19653
21190
|
}
|
|
19654
21191
|
await loadEnvFile(resolve11(process.cwd(), ".env.local"));
|
|
19655
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
|
+
}
|
|
19656
21203
|
if (isWorktreeCommand(parsed.command)) {
|
|
19657
21204
|
const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
|
|
19658
21205
|
const exitCode2 = await runWorktreeCommand2({
|
|
@@ -19701,8 +21248,8 @@ async function main(args = process.argv.slice(2)) {
|
|
|
19701
21248
|
}
|
|
19702
21249
|
process.on("SIGINT", cleanup);
|
|
19703
21250
|
process.on("SIGTERM", cleanup);
|
|
19704
|
-
const backendEntry =
|
|
19705
|
-
const staticDir =
|
|
21251
|
+
const backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
|
|
21252
|
+
const staticDir = join10(PKG_ROOT, "frontend", "dist");
|
|
19706
21253
|
if (!existsSync5(staticDir)) {
|
|
19707
21254
|
console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
|
|
19708
21255
|
process.exit(1);
|