@calltelemetry/openclaw-linear 0.4.0 → 0.4.1
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/index.ts +4 -0
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
- package/src/active-session.ts +40 -0
- package/src/code-tool.ts +2 -2
- package/src/codex-worktree.ts +162 -36
- package/src/dispatch-service.ts +113 -0
- package/src/dispatch-state.ts +265 -0
- package/src/pipeline.ts +311 -82
- package/src/tier-assess.ts +157 -0
- package/src/webhook.ts +223 -197
package/src/webhook.ts
CHANGED
|
@@ -3,9 +3,11 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
5
|
import { LinearAgentApi, resolveLinearToken } from "./linear-api.js";
|
|
6
|
-
|
|
7
|
-
// import { runFullPipeline, resumePipeline, type PipelineContext } from "./pipeline.js";
|
|
6
|
+
import { runFullPipeline, type PipelineContext } from "./pipeline.js";
|
|
8
7
|
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
8
|
+
import { readDispatchState, getActiveDispatch, registerDispatch, updateDispatchStatus, completeDispatch, removeActiveDispatch } from "./dispatch-state.js";
|
|
9
|
+
import { assessTier } from "./tier-assess.js";
|
|
10
|
+
import { createWorktree, prepareWorkspace } from "./codex-worktree.js";
|
|
9
11
|
|
|
10
12
|
// ── Agent profiles (loaded from config, no hardcoded names) ───────
|
|
11
13
|
interface AgentProfile {
|
|
@@ -858,7 +860,7 @@ export async function handleLinearWebhook(
|
|
|
858
860
|
}
|
|
859
861
|
|
|
860
862
|
const trigger = isDelegatedToUs ? "delegated" : "assigned";
|
|
861
|
-
api.logger.info(`Issue ${trigger} to our app user (${viewerId}),
|
|
863
|
+
api.logger.info(`Issue ${trigger} to our app user (${viewerId}), executing pipeline`);
|
|
862
864
|
|
|
863
865
|
// Dedup on assignment/delegation
|
|
864
866
|
const dedupKey = `${trigger}:${issue.id}:${viewerId}`;
|
|
@@ -867,200 +869,11 @@ export async function handleLinearWebhook(
|
|
|
867
869
|
return true;
|
|
868
870
|
}
|
|
869
871
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
try {
|
|
876
|
-
enrichedIssue = await linearApi.getIssueDetails(issue.id);
|
|
877
|
-
if (enrichedIssue?.team?.id) {
|
|
878
|
-
teamLabels = await linearApi.getTeamLabels(enrichedIssue.team.id);
|
|
879
|
-
}
|
|
880
|
-
} catch (err) {
|
|
881
|
-
api.logger.warn(`Could not fetch issue details: ${err}`);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
|
|
885
|
-
const comments = enrichedIssue?.comments?.nodes ?? [];
|
|
886
|
-
const commentSummary = comments
|
|
887
|
-
.slice(-5)
|
|
888
|
-
.map((c: any) => ` - **${c.user?.name ?? "Unknown"}**: ${c.body?.slice(0, 200)}`)
|
|
889
|
-
.join("\n");
|
|
890
|
-
|
|
891
|
-
const estimationType = enrichedIssue?.team?.issueEstimationType ?? "fibonacci";
|
|
892
|
-
const currentLabels = enrichedIssue?.labels?.nodes ?? [];
|
|
893
|
-
const currentLabelNames = currentLabels.map((l: any) => l.name).join(", ") || "None";
|
|
894
|
-
const availableLabelList = teamLabels.map((l) => ` - "${l.name}" (id: ${l.id})`).join("\n");
|
|
895
|
-
|
|
896
|
-
const message = [
|
|
897
|
-
`IMPORTANT: You are triaging a delegated Linear issue. You MUST respond with a JSON block containing your triage decisions, followed by your assessment as plain text.`,
|
|
898
|
-
``,
|
|
899
|
-
`## Issue: ${enrichedIssue?.identifier ?? issue.identifier ?? issue.id} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
900
|
-
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Current Estimate:** ${enrichedIssue?.estimate ?? "None"} | **Current Labels:** ${currentLabelNames}`,
|
|
901
|
-
``,
|
|
902
|
-
`**Description:**`,
|
|
903
|
-
description,
|
|
904
|
-
commentSummary ? `\n**Recent comments:**\n${commentSummary}` : "",
|
|
905
|
-
``,
|
|
906
|
-
`## Your Triage Tasks`,
|
|
907
|
-
``,
|
|
908
|
-
`1. **Story Points** — Estimate complexity using ${estimationType} scale (1=trivial, 2=small, 3=medium, 5=large, 8=very large, 13=epic)`,
|
|
909
|
-
`2. **Labels** — Select appropriate labels from the team's available labels`,
|
|
910
|
-
`3. **Assessment** — Brief analysis of what this issue needs`,
|
|
911
|
-
``,
|
|
912
|
-
`## Available Labels`,
|
|
913
|
-
availableLabelList || " (no labels configured)",
|
|
914
|
-
``,
|
|
915
|
-
`## Response Format`,
|
|
916
|
-
``,
|
|
917
|
-
`You MUST start your response with a JSON block, then follow with your assessment:`,
|
|
918
|
-
``,
|
|
919
|
-
'```json',
|
|
920
|
-
`{`,
|
|
921
|
-
` "estimate": <number>,`,
|
|
922
|
-
` "labelIds": ["<id1>", "<id2>"],`,
|
|
923
|
-
` "assessment": "<one-line summary of your sizing rationale>"`,
|
|
924
|
-
`}`,
|
|
925
|
-
'```',
|
|
926
|
-
``,
|
|
927
|
-
`Then write your full assessment as markdown below the JSON block.`,
|
|
928
|
-
].filter(Boolean).join("\n");
|
|
929
|
-
|
|
930
|
-
// Dispatch agent with session lifecycle (non-blocking)
|
|
931
|
-
void (async () => {
|
|
932
|
-
const profiles = loadAgentProfiles();
|
|
933
|
-
const defaultProfile = Object.entries(profiles).find(([, p]) => p.isDefault);
|
|
934
|
-
const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
|
|
935
|
-
const avatarUrl = defaultProfile?.[1]?.avatarUrl ?? profiles[agentId]?.avatarUrl;
|
|
936
|
-
let agentSessionId: string | null = null;
|
|
937
|
-
|
|
938
|
-
try {
|
|
939
|
-
const sessionResult = await linearApi.createSessionOnIssue(issue.id);
|
|
940
|
-
agentSessionId = sessionResult.sessionId;
|
|
941
|
-
if (agentSessionId) {
|
|
942
|
-
wasRecentlyProcessed(`session:${agentSessionId}`);
|
|
943
|
-
api.logger.info(`Created agent session ${agentSessionId} for ${trigger}`);
|
|
944
|
-
setActiveSession({
|
|
945
|
-
agentSessionId,
|
|
946
|
-
issueIdentifier: enrichedIssue?.identifier ?? issue.identifier ?? issue.id,
|
|
947
|
-
issueId: issue.id,
|
|
948
|
-
agentId,
|
|
949
|
-
startedAt: Date.now(),
|
|
950
|
-
});
|
|
951
|
-
} else {
|
|
952
|
-
api.logger.warn(`Could not create agent session for assignment: ${sessionResult.error ?? "unknown"}`);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (agentSessionId) {
|
|
956
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
957
|
-
type: "thought",
|
|
958
|
-
body: `Reviewing assigned issue ${enrichedIssue?.identifier ?? issue.id}...`,
|
|
959
|
-
}).catch(() => {});
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (agentSessionId) {
|
|
963
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
964
|
-
type: "action",
|
|
965
|
-
action: "Triaging",
|
|
966
|
-
parameter: `${enrichedIssue?.identifier ?? issue.id} — estimating, labeling, sizing`,
|
|
967
|
-
}).catch(() => {});
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const sessionId = `linear-assign-${issue.id}-${Date.now()}`;
|
|
971
|
-
const { runAgent } = await import("./agent.js");
|
|
972
|
-
const result = await runAgent({
|
|
973
|
-
api,
|
|
974
|
-
agentId,
|
|
975
|
-
sessionId,
|
|
976
|
-
message,
|
|
977
|
-
timeoutMs: 3 * 60_000,
|
|
978
|
-
streaming: agentSessionId ? { linearApi, agentSessionId } : undefined,
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
const responseBody = result.success
|
|
982
|
-
? result.output
|
|
983
|
-
: `I encountered an error reviewing this assignment. Please try again.`;
|
|
984
|
-
|
|
985
|
-
// Parse triage JSON from agent response and apply to issue
|
|
986
|
-
let commentBody = responseBody;
|
|
987
|
-
if (result.success) {
|
|
988
|
-
const jsonMatch = responseBody.match(/```json\s*\n?([\s\S]*?)\n?```/);
|
|
989
|
-
if (jsonMatch) {
|
|
990
|
-
try {
|
|
991
|
-
const triage = JSON.parse(jsonMatch[1]);
|
|
992
|
-
const updateInput: Record<string, unknown> = {};
|
|
993
|
-
|
|
994
|
-
if (typeof triage.estimate === "number") {
|
|
995
|
-
updateInput.estimate = triage.estimate;
|
|
996
|
-
}
|
|
997
|
-
if (Array.isArray(triage.labelIds) && triage.labelIds.length > 0) {
|
|
998
|
-
// Merge with existing labels
|
|
999
|
-
const existingIds = currentLabels.map((l: any) => l.id);
|
|
1000
|
-
const allIds = [...new Set([...existingIds, ...triage.labelIds])];
|
|
1001
|
-
updateInput.labelIds = allIds;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
if (Object.keys(updateInput).length > 0) {
|
|
1005
|
-
await linearApi.updateIssue(issue.id, updateInput);
|
|
1006
|
-
api.logger.info(`Applied triage to ${enrichedIssue?.identifier ?? issue.id}: ${JSON.stringify(updateInput)}`);
|
|
1007
|
-
|
|
1008
|
-
if (agentSessionId) {
|
|
1009
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
1010
|
-
type: "action",
|
|
1011
|
-
action: "Applied triage",
|
|
1012
|
-
result: `estimate=${triage.estimate ?? "unchanged"}, labels=${triage.labelIds?.length ?? 0} added`,
|
|
1013
|
-
}).catch(() => {});
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// Strip the JSON block from the comment — post only the assessment
|
|
1018
|
-
commentBody = responseBody.replace(/```json\s*\n?[\s\S]*?\n?```\s*\n?/, "").trim();
|
|
1019
|
-
} catch (parseErr) {
|
|
1020
|
-
api.logger.warn(`Could not parse triage JSON: ${parseErr}`);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// Post comment with assessment
|
|
1026
|
-
const brandingOpts = avatarUrl
|
|
1027
|
-
? { createAsUser: label, displayIconUrl: avatarUrl }
|
|
1028
|
-
: undefined;
|
|
1029
|
-
|
|
1030
|
-
try {
|
|
1031
|
-
if (brandingOpts) {
|
|
1032
|
-
await linearApi.createComment(issue.id, commentBody, brandingOpts);
|
|
1033
|
-
} else {
|
|
1034
|
-
await linearApi.createComment(issue.id, `**[${label}]** ${commentBody}`);
|
|
1035
|
-
}
|
|
1036
|
-
} catch (brandErr) {
|
|
1037
|
-
api.logger.warn(`Branded comment failed, falling back to prefix: ${brandErr}`);
|
|
1038
|
-
await linearApi.createComment(issue.id, `**[${label}]** ${commentBody}`);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
if (agentSessionId) {
|
|
1042
|
-
const truncated = commentBody.length > 2000
|
|
1043
|
-
? commentBody.slice(0, 2000) + "…"
|
|
1044
|
-
: commentBody;
|
|
1045
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
1046
|
-
type: "response",
|
|
1047
|
-
body: truncated,
|
|
1048
|
-
}).catch(() => {});
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
api.logger.info(`Posted assignment response to ${enrichedIssue?.identifier ?? issue.id}`);
|
|
1052
|
-
} catch (err) {
|
|
1053
|
-
api.logger.error(`Issue assignment handler error: ${err}`);
|
|
1054
|
-
if (agentSessionId) {
|
|
1055
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
1056
|
-
type: "error",
|
|
1057
|
-
body: `Failed to process assignment: ${String(err).slice(0, 500)}`,
|
|
1058
|
-
}).catch(() => {});
|
|
1059
|
-
}
|
|
1060
|
-
} finally {
|
|
1061
|
-
clearActiveSession(issue.id);
|
|
1062
|
-
}
|
|
1063
|
-
})();
|
|
872
|
+
// Assignment triggers the full dispatch pipeline:
|
|
873
|
+
// tier assessment → worktree → plan → implement → audit
|
|
874
|
+
void handleDispatch(api, linearApi, issue).catch((err) => {
|
|
875
|
+
api.logger.error(`Dispatch pipeline error for ${issue.identifier ?? issue.id}: ${err}`);
|
|
876
|
+
});
|
|
1064
877
|
|
|
1065
878
|
return true;
|
|
1066
879
|
}
|
|
@@ -1290,3 +1103,216 @@ export async function handleLinearWebhook(
|
|
|
1290
1103
|
res.end("ok");
|
|
1291
1104
|
return true;
|
|
1292
1105
|
}
|
|
1106
|
+
|
|
1107
|
+
// ── @dispatch handler ─────────────────────────────────────────────
|
|
1108
|
+
//
|
|
1109
|
+
// Triggered by `@dispatch` in a Linear comment. Assesses issue complexity,
|
|
1110
|
+
// creates a persistent worktree, registers the dispatch in state, and
|
|
1111
|
+
// launches the pipeline (plan → implement → audit).
|
|
1112
|
+
|
|
1113
|
+
async function handleDispatch(
|
|
1114
|
+
api: OpenClawPluginApi,
|
|
1115
|
+
linearApi: LinearAgentApi,
|
|
1116
|
+
issue: any,
|
|
1117
|
+
): Promise<void> {
|
|
1118
|
+
const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
|
|
1119
|
+
const statePath = pluginConfig?.dispatchStatePath as string | undefined;
|
|
1120
|
+
const worktreeBaseDir = pluginConfig?.worktreeBaseDir as string | undefined;
|
|
1121
|
+
const baseRepo = (pluginConfig?.codexBaseRepo as string) ?? "/home/claw/ai-workspace";
|
|
1122
|
+
const identifier = issue.identifier ?? issue.id;
|
|
1123
|
+
|
|
1124
|
+
api.logger.info(`@dispatch: processing ${identifier}`);
|
|
1125
|
+
|
|
1126
|
+
// 1. Check for existing active dispatch — reclaim if stale
|
|
1127
|
+
const STALE_DISPATCH_MS = 30 * 60_000; // 30 min without a gateway holding it = stale
|
|
1128
|
+
const state = await readDispatchState(statePath);
|
|
1129
|
+
const existing = getActiveDispatch(state, identifier);
|
|
1130
|
+
if (existing) {
|
|
1131
|
+
const ageMs = Date.now() - new Date(existing.dispatchedAt).getTime();
|
|
1132
|
+
const isStale = ageMs > STALE_DISPATCH_MS;
|
|
1133
|
+
const inMemory = activeRuns.has(issue.id);
|
|
1134
|
+
|
|
1135
|
+
if (!isStale && inMemory) {
|
|
1136
|
+
// Truly still running in this gateway process
|
|
1137
|
+
api.logger.info(`dispatch: ${identifier} actively running (status: ${existing.status}, age: ${Math.round(ageMs / 1000)}s) — skipping`);
|
|
1138
|
+
await linearApi.createComment(
|
|
1139
|
+
issue.id,
|
|
1140
|
+
`Already running as **${existing.tier}** (status: ${existing.status}, started ${Math.round(ageMs / 60_000)}m ago). Worktree: \`${existing.worktreePath}\``,
|
|
1141
|
+
);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Stale or not in memory (gateway restarted) — reclaim
|
|
1146
|
+
api.logger.info(
|
|
1147
|
+
`dispatch: ${identifier} reclaiming stale dispatch (status: ${existing.status}, ` +
|
|
1148
|
+
`age: ${Math.round(ageMs / 1000)}s, inMemory: ${inMemory}, stale: ${isStale})`,
|
|
1149
|
+
);
|
|
1150
|
+
await removeActiveDispatch(identifier, statePath);
|
|
1151
|
+
activeRuns.delete(issue.id);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// 2. Prevent concurrent runs on same issue
|
|
1155
|
+
if (activeRuns.has(issue.id)) {
|
|
1156
|
+
api.logger.info(`@dispatch: ${identifier} has active agent run — skipping`);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// 3. Fetch full issue details for tier assessment
|
|
1161
|
+
let enrichedIssue: any;
|
|
1162
|
+
try {
|
|
1163
|
+
enrichedIssue = await linearApi.getIssueDetails(issue.id);
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
api.logger.error(`@dispatch: failed to fetch issue details: ${err}`);
|
|
1166
|
+
enrichedIssue = issue;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const labels = enrichedIssue.labels?.nodes?.map((l: any) => l.name) ?? [];
|
|
1170
|
+
const commentCount = enrichedIssue.comments?.nodes?.length ?? 0;
|
|
1171
|
+
|
|
1172
|
+
// 4. Assess complexity tier
|
|
1173
|
+
const assessment = await assessTier(api, {
|
|
1174
|
+
identifier,
|
|
1175
|
+
title: enrichedIssue.title ?? "(untitled)",
|
|
1176
|
+
description: enrichedIssue.description,
|
|
1177
|
+
labels,
|
|
1178
|
+
commentCount,
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
api.logger.info(`@dispatch: ${identifier} assessed as ${assessment.tier} (${assessment.model}) — ${assessment.reasoning}`);
|
|
1182
|
+
|
|
1183
|
+
// 5. Create persistent worktree
|
|
1184
|
+
let worktree;
|
|
1185
|
+
try {
|
|
1186
|
+
worktree = createWorktree(identifier, { baseRepo, baseDir: worktreeBaseDir });
|
|
1187
|
+
api.logger.info(`@dispatch: worktree ${worktree.resumed ? "resumed" : "created"} at ${worktree.path}`);
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
api.logger.error(`@dispatch: worktree creation failed: ${err}`);
|
|
1190
|
+
await linearApi.createComment(
|
|
1191
|
+
issue.id,
|
|
1192
|
+
`Dispatch failed — could not create worktree: ${String(err).slice(0, 200)}`,
|
|
1193
|
+
);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// 5b. Prepare workspace: pull latest from origin + init submodules
|
|
1198
|
+
const prep = prepareWorkspace(worktree.path, worktree.branch);
|
|
1199
|
+
if (prep.errors.length > 0) {
|
|
1200
|
+
api.logger.warn(`@dispatch: workspace prep had errors: ${prep.errors.join("; ")}`);
|
|
1201
|
+
} else {
|
|
1202
|
+
api.logger.info(
|
|
1203
|
+
`@dispatch: workspace prepared — pulled=${prep.pulled}, submodules=${prep.submodulesInitialized}`,
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// 6. Create agent session on Linear
|
|
1208
|
+
let agentSessionId: string | undefined;
|
|
1209
|
+
try {
|
|
1210
|
+
const sessionResult = await linearApi.createSessionOnIssue(issue.id);
|
|
1211
|
+
agentSessionId = sessionResult.sessionId ?? undefined;
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
api.logger.warn(`@dispatch: could not create agent session: ${err}`);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// 7. Register dispatch in persistent state
|
|
1217
|
+
const now = new Date().toISOString();
|
|
1218
|
+
await registerDispatch(identifier, {
|
|
1219
|
+
issueId: issue.id,
|
|
1220
|
+
issueIdentifier: identifier,
|
|
1221
|
+
worktreePath: worktree.path,
|
|
1222
|
+
branch: worktree.branch,
|
|
1223
|
+
tier: assessment.tier,
|
|
1224
|
+
model: assessment.model,
|
|
1225
|
+
status: "dispatched",
|
|
1226
|
+
dispatchedAt: now,
|
|
1227
|
+
agentSessionId,
|
|
1228
|
+
}, statePath);
|
|
1229
|
+
|
|
1230
|
+
// 8. Register active session for tool resolution
|
|
1231
|
+
setActiveSession({
|
|
1232
|
+
agentSessionId: agentSessionId ?? "",
|
|
1233
|
+
issueIdentifier: identifier,
|
|
1234
|
+
issueId: issue.id,
|
|
1235
|
+
agentId: resolveAgentId(api),
|
|
1236
|
+
startedAt: Date.now(),
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// 9. Post dispatch confirmation comment
|
|
1240
|
+
const prepStatus = prep.errors.length > 0
|
|
1241
|
+
? `Workspace prep: partial (${prep.errors.join("; ")})`
|
|
1242
|
+
: `Workspace prep: OK (pulled=${prep.pulled}, submodules=${prep.submodulesInitialized})`;
|
|
1243
|
+
const statusComment = [
|
|
1244
|
+
`**Dispatched** as **${assessment.tier}** (${assessment.model})`,
|
|
1245
|
+
`> ${assessment.reasoning}`,
|
|
1246
|
+
``,
|
|
1247
|
+
`Worktree: \`${worktree.path}\` ${worktree.resumed ? "(resumed)" : "(fresh)"}`,
|
|
1248
|
+
`Branch: \`${worktree.branch}\``,
|
|
1249
|
+
prepStatus,
|
|
1250
|
+
].join("\n");
|
|
1251
|
+
|
|
1252
|
+
await linearApi.createComment(issue.id, statusComment);
|
|
1253
|
+
|
|
1254
|
+
if (agentSessionId) {
|
|
1255
|
+
await linearApi.emitActivity(agentSessionId, {
|
|
1256
|
+
type: "thought",
|
|
1257
|
+
body: `Dispatching ${identifier} as ${assessment.tier}...`,
|
|
1258
|
+
}).catch(() => {});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// 10. Apply tier label (best effort)
|
|
1262
|
+
try {
|
|
1263
|
+
if (enrichedIssue.team?.id) {
|
|
1264
|
+
const teamLabels = await linearApi.getTeamLabels(enrichedIssue.team.id);
|
|
1265
|
+
const tierLabel = teamLabels.find((l: any) => l.name === `developer:${assessment.tier}`);
|
|
1266
|
+
if (tierLabel) {
|
|
1267
|
+
const currentLabelIds = enrichedIssue.labels?.nodes?.map((l: any) => l.id) ?? [];
|
|
1268
|
+
await linearApi.updateIssue(issue.id, {
|
|
1269
|
+
labelIds: [...currentLabelIds, tierLabel.id],
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
api.logger.warn(`@dispatch: could not apply tier label: ${err}`);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// 11. Run pipeline (non-blocking)
|
|
1278
|
+
const agentId = resolveAgentId(api);
|
|
1279
|
+
const pipelineCtx: PipelineContext = {
|
|
1280
|
+
api,
|
|
1281
|
+
linearApi,
|
|
1282
|
+
agentSessionId: agentSessionId ?? `dispatch-${identifier}-${Date.now()}`,
|
|
1283
|
+
agentId,
|
|
1284
|
+
issue: {
|
|
1285
|
+
id: issue.id,
|
|
1286
|
+
identifier,
|
|
1287
|
+
title: enrichedIssue.title ?? "(untitled)",
|
|
1288
|
+
description: enrichedIssue.description,
|
|
1289
|
+
},
|
|
1290
|
+
worktreePath: worktree.path,
|
|
1291
|
+
codexBranch: worktree.branch,
|
|
1292
|
+
tier: assessment.tier,
|
|
1293
|
+
model: assessment.model,
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
activeRuns.add(issue.id);
|
|
1297
|
+
|
|
1298
|
+
// Update status to running
|
|
1299
|
+
await updateDispatchStatus(identifier, "running", statePath);
|
|
1300
|
+
|
|
1301
|
+
runFullPipeline(pipelineCtx)
|
|
1302
|
+
.then(async () => {
|
|
1303
|
+
await completeDispatch(identifier, {
|
|
1304
|
+
tier: assessment.tier,
|
|
1305
|
+
status: "done",
|
|
1306
|
+
completedAt: new Date().toISOString(),
|
|
1307
|
+
}, statePath);
|
|
1308
|
+
api.logger.info(`@dispatch: pipeline completed for ${identifier}`);
|
|
1309
|
+
})
|
|
1310
|
+
.catch(async (err) => {
|
|
1311
|
+
api.logger.error(`@dispatch: pipeline failed for ${identifier}: ${err}`);
|
|
1312
|
+
await updateDispatchStatus(identifier, "failed", statePath);
|
|
1313
|
+
})
|
|
1314
|
+
.finally(() => {
|
|
1315
|
+
activeRuns.delete(issue.id);
|
|
1316
|
+
clearActiveSession(issue.id);
|
|
1317
|
+
});
|
|
1318
|
+
}
|