@defend-tech/opencode-optima 0.1.34 → 0.1.36
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/dist/index.js +180 -13
- package/dist/sanitize_cli.js +180 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7935,6 +7935,8 @@ var CLICKUP_PM_MENTION_NAME = "Defend Tech Product Manager";
|
|
|
7935
7935
|
var CLICKUP_WEBHOOK_RUNTIME_VERSION = 1;
|
|
7936
7936
|
var CLICKUP_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
7937
7937
|
var CLICKUP_WEBHOOK_REQUEST_TIMEOUT_MS = 1e4;
|
|
7938
|
+
var CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT = 50;
|
|
7939
|
+
var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
|
|
7938
7940
|
var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
|
|
7939
7941
|
var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
|
|
7940
7942
|
var DISCUSSION_BACKFILL_FETCH_LIMIT = 100;
|
|
@@ -9053,6 +9055,7 @@ function sanitizeClickUpWebhookState(state = {}, config = null) {
|
|
|
9053
9055
|
lastValidatedAt: state.lastValidatedAt || null,
|
|
9054
9056
|
listener: isPlainObject(state.listener) ? state.listener : {},
|
|
9055
9057
|
pendingSessions: isPlainObject(state.pendingSessions) ? state.pendingSessions : {},
|
|
9058
|
+
lastWebhookAt: state.lastWebhookAt || state.last_webhook_at || null,
|
|
9056
9059
|
recentEventKeys
|
|
9057
9060
|
};
|
|
9058
9061
|
}
|
|
@@ -9135,6 +9138,9 @@ function createClickUpApiClient(config, fetchImpl = globalThis.fetch) {
|
|
|
9135
9138
|
async deleteWebhook(webhookId) {
|
|
9136
9139
|
return request(`https://api.clickup.com/api/v2/webhook/${encodeURIComponent(webhookId)}`, { method: "DELETE" });
|
|
9137
9140
|
},
|
|
9141
|
+
async getAuthorizedUser() {
|
|
9142
|
+
return request("https://api.clickup.com/api/v2/user");
|
|
9143
|
+
},
|
|
9138
9144
|
async getTask(taskId) {
|
|
9139
9145
|
return request(`https://api.clickup.com/api/v2/task/${encodeURIComponent(taskId)}`);
|
|
9140
9146
|
},
|
|
@@ -9149,6 +9155,20 @@ function createClickUpApiClient(config, fetchImpl = globalThis.fetch) {
|
|
|
9149
9155
|
method: "POST",
|
|
9150
9156
|
body: JSON.stringify({ comment_text: comment })
|
|
9151
9157
|
});
|
|
9158
|
+
},
|
|
9159
|
+
async listAssignedTasks({ assigneeId, statuses = [], limit = CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT } = {}) {
|
|
9160
|
+
const params = new URLSearchParams();
|
|
9161
|
+
if (assigneeId) params.append("assignees[]", assigneeId);
|
|
9162
|
+
for (const status of statuses) if (status) params.append("statuses[]", status);
|
|
9163
|
+
if (limit) params.append("limit", String(limit));
|
|
9164
|
+
return request(`https://api.clickup.com/api/v2/team/${encodeURIComponent(config.teamId)}/task?${params.toString()}`);
|
|
9165
|
+
},
|
|
9166
|
+
async getTaskComments({ taskId, start, limit = CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT } = {}) {
|
|
9167
|
+
const params = new URLSearchParams();
|
|
9168
|
+
if (start) params.set("start", String(start));
|
|
9169
|
+
if (limit) params.set("limit", String(limit));
|
|
9170
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
9171
|
+
return request(`https://api.clickup.com/api/v2/task/${encodeURIComponent(taskId)}/comment${suffix}`);
|
|
9152
9172
|
}
|
|
9153
9173
|
};
|
|
9154
9174
|
}
|
|
@@ -9194,7 +9214,7 @@ async function findReusableClickUpWebhook(config, clickupClient = null) {
|
|
|
9194
9214
|
}
|
|
9195
9215
|
return null;
|
|
9196
9216
|
}
|
|
9197
|
-
async function validateClickUpWebhookState(state, config, clickupClient = null) {
|
|
9217
|
+
async function validateClickUpWebhookState(state, config, clickupClient = null, { allowRemoteUnhealthyLocalRecovery = false } = {}) {
|
|
9198
9218
|
if (!isClickUpWebhookStateActive(state, config)) return { valid: false, reason: "state_incomplete" };
|
|
9199
9219
|
if (clickupClient?.listWebhooks) {
|
|
9200
9220
|
const listed = await clickupClient.listWebhooks({ teamId: config.teamId });
|
|
@@ -9202,10 +9222,22 @@ async function validateClickUpWebhookState(state, config, clickupClient = null)
|
|
|
9202
9222
|
if (!match) return { valid: false, reason: "remote_state_missing" };
|
|
9203
9223
|
const remote = normalizeClickUpWebhookApiResponse(match, config);
|
|
9204
9224
|
const remoteWithLocalSecret = { ...remote, secret: state.secret };
|
|
9205
|
-
if (
|
|
9206
|
-
return { valid:
|
|
9225
|
+
if (isClickUpWebhookStateActive(remoteWithLocalSecret, config) && remote.webhookId === state.webhookId) {
|
|
9226
|
+
return { valid: true, mode: "remote", state: { ...remoteWithLocalSecret, recentEventKeys: state.recentEventKeys || [], lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
9227
|
+
}
|
|
9228
|
+
const localStateValidation = await validateClickUpWebhookState(state, config, null);
|
|
9229
|
+
if (allowRemoteUnhealthyLocalRecovery && localStateValidation.valid && remote.webhookId === state.webhookId) {
|
|
9230
|
+
const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && [...new Set(config.webhook.events || [])].every((event) => new Set(remote.events || []).has(event));
|
|
9231
|
+
if (remoteConfigMatches) {
|
|
9232
|
+
return {
|
|
9233
|
+
...localStateValidation,
|
|
9234
|
+
mode: "local_state_remote_unhealthy",
|
|
9235
|
+
limitation: "ClickUp remote webhook is present but unhealthy; Optima starts the local listener from matching local id/secret/config so delivery can recover.",
|
|
9236
|
+
remote
|
|
9237
|
+
};
|
|
9238
|
+
}
|
|
9207
9239
|
}
|
|
9208
|
-
return { valid:
|
|
9240
|
+
return { valid: false, reason: "remote_state_mismatch", remote };
|
|
9209
9241
|
}
|
|
9210
9242
|
return { valid: true, mode: "local_state", state: { ...state, lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString() }, limitation: "ClickUp API validation unavailable; Optima trusts ignored local runtime state only after id/secret/config checks." };
|
|
9211
9243
|
}
|
|
@@ -9215,7 +9247,7 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
|
|
|
9215
9247
|
}
|
|
9216
9248
|
const { config } = validation;
|
|
9217
9249
|
const existing = readClickUpWebhookState(worktree, config);
|
|
9218
|
-
const existingValidation = await validateClickUpWebhookState(existing, config, clickupClient);
|
|
9250
|
+
const existingValidation = await validateClickUpWebhookState(existing, config, clickupClient, { allowRemoteUnhealthyLocalRecovery: true });
|
|
9219
9251
|
if (existingValidation.valid) {
|
|
9220
9252
|
const state = writeClickUpWebhookState(worktree, { ...existingValidation.state, recentEventKeys: existing.recentEventKeys || [] }, config);
|
|
9221
9253
|
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: existingValidation.mode });
|
|
@@ -9366,6 +9398,31 @@ function recordClickUpCommentVersionProcessed({ ledgerPath, key, taskId, eventTy
|
|
|
9366
9398
|
function clickUpTaskIdFromPayload(payload = {}) {
|
|
9367
9399
|
return String(payload.task_id || payload.taskId || payload.task?.id || payload.task?.task_id || "").trim();
|
|
9368
9400
|
}
|
|
9401
|
+
function clickUpTaskListItems(response = {}) {
|
|
9402
|
+
if (Array.isArray(response)) return response;
|
|
9403
|
+
if (Array.isArray(response.tasks)) return response.tasks;
|
|
9404
|
+
if (Array.isArray(response.data)) return response.data;
|
|
9405
|
+
if (Array.isArray(response?.data?.tasks)) return response.data.tasks;
|
|
9406
|
+
return [];
|
|
9407
|
+
}
|
|
9408
|
+
function clickUpCommentListItems(response = {}) {
|
|
9409
|
+
if (Array.isArray(response)) return response;
|
|
9410
|
+
if (Array.isArray(response.comments)) return response.comments;
|
|
9411
|
+
if (Array.isArray(response.data)) return response.data;
|
|
9412
|
+
if (Array.isArray(response?.data?.comments)) return response.data.comments;
|
|
9413
|
+
return [];
|
|
9414
|
+
}
|
|
9415
|
+
function clickUpTimestampMs(value) {
|
|
9416
|
+
if (value === null || value === void 0 || value === "") return 0;
|
|
9417
|
+
if (value instanceof Date) return value.getTime();
|
|
9418
|
+
const numeric = Number(value);
|
|
9419
|
+
if (Number.isFinite(numeric) && numeric > 0) return numeric < 1e10 ? numeric * 1e3 : numeric;
|
|
9420
|
+
const parsed = Date.parse(String(value));
|
|
9421
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
9422
|
+
}
|
|
9423
|
+
function clickUpCommentUpdatedMs(comment = {}) {
|
|
9424
|
+
return clickUpTimestampMs(comment.date_updated || comment.dateUpdated || comment.updated_at || comment.updatedAt || comment.date || comment.date_created || comment.created_at);
|
|
9425
|
+
}
|
|
9369
9426
|
function clickUpTaskName(task = {}) {
|
|
9370
9427
|
return String(task?.name || "").trim();
|
|
9371
9428
|
}
|
|
@@ -9758,12 +9815,18 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
|
|
|
9758
9815
|
if (!taskId) return { ok: false, action: "error", reason: "missing_task_id" };
|
|
9759
9816
|
let task = payload.task || (clickupClient?.getTask ? await clickupClient.getTask(taskId) : null);
|
|
9760
9817
|
if (!task) return { ok: false, action: "error", reason: "task_unavailable", taskId };
|
|
9818
|
+
if (clickupClient?.getTask) {
|
|
9819
|
+
const latestTask = await clickupClient.getTask(taskId);
|
|
9820
|
+
if (latestTask) task = latestTask;
|
|
9821
|
+
}
|
|
9761
9822
|
const isCommentEvent = eventType === "taskCommentPosted" || eventType === "taskCommentUpdated";
|
|
9762
9823
|
if (isCommentEvent) {
|
|
9763
9824
|
const comment = clickUpCommentFromPayload(payload);
|
|
9764
9825
|
const authorId = clickUpCommentAuthorId(comment);
|
|
9765
9826
|
if (authorId && authorId === config.routing.ignoredCommentAuthorId) return finish({ ok: true, action: "ignored", reason: "self_authored_comment", taskId });
|
|
9766
|
-
|
|
9827
|
+
const mentionsProductManager = clickUpCommentMentionsProductManager(comment, config.routing);
|
|
9828
|
+
const assignedToProductManager = isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId);
|
|
9829
|
+
if (!mentionsProductManager && !assignedToProductManager) return finish({ ok: true, action: "ignored", reason: "missing_product_manager_mention_or_assignment", taskId });
|
|
9767
9830
|
commentLedgerKey = clickUpCommentLedgerKey({ taskId, eventType, payload });
|
|
9768
9831
|
try {
|
|
9769
9832
|
if (isClickUpCommentVersionProcessed({ ledgerPath: commentLedgerPath, key: commentLedgerKey, worktree })) {
|
|
@@ -9775,10 +9838,6 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
|
|
|
9775
9838
|
}
|
|
9776
9839
|
if (!isCommentEvent && !isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) return finish({ ok: true, action: "ignored", reason: "not_assigned_to_product_manager", taskId });
|
|
9777
9840
|
if (isClickUpTaskTerminal(task, payload, config.routing.ignoredStatuses)) return finish({ ok: true, action: "ignored", reason: "terminal_status", taskId });
|
|
9778
|
-
if (clickupClient?.getTask) {
|
|
9779
|
-
const latestTask = await clickupClient.getTask(taskId);
|
|
9780
|
-
if (latestTask) task = latestTask;
|
|
9781
|
-
}
|
|
9782
9841
|
const sessionTitle = formatClickUpSessionTitle({ taskId, payloadTask: payload.task, task });
|
|
9783
9842
|
const existingMetadata = clickUpTaskAgentMetadata(task, config.routing.metadataFieldId);
|
|
9784
9843
|
const metadata = normalizeAgentMetadataJson(existingMetadata);
|
|
@@ -9846,6 +9905,94 @@ async function routeClickUpWebhookEvent(options = {}) {
|
|
|
9846
9905
|
const taskId = clickUpTaskIdFromPayload(options.payload || {});
|
|
9847
9906
|
return withClickUpTaskRouteLock(taskId, () => routeClickUpWebhookEventUnlocked(options));
|
|
9848
9907
|
}
|
|
9908
|
+
async function reconcileClickUpStartup({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, saveState = null, now = () => /* @__PURE__ */ new Date(), limit = CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT } = {}) {
|
|
9909
|
+
if (!config || !clickupClient?.listAssignedTasks) return { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
|
|
9910
|
+
let authorizedUserId = "";
|
|
9911
|
+
if (clickupClient?.getAuthorizedUser) {
|
|
9912
|
+
try {
|
|
9913
|
+
const userResponse = await clickupClient.getAuthorizedUser();
|
|
9914
|
+
authorizedUserId = String(userResponse?.user?.id || userResponse?.id || "").trim();
|
|
9915
|
+
} catch (error) {
|
|
9916
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_user_lookup_failed", message: error.message });
|
|
9917
|
+
}
|
|
9918
|
+
}
|
|
9919
|
+
let mutableState = state;
|
|
9920
|
+
const persistState = (nextState) => {
|
|
9921
|
+
mutableState = nextState;
|
|
9922
|
+
if (saveState) saveState(nextState);
|
|
9923
|
+
};
|
|
9924
|
+
const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
|
|
9925
|
+
const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
|
|
9926
|
+
const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0 };
|
|
9927
|
+
clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
|
|
9928
|
+
const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
|
|
9929
|
+
const tasks = clickUpTaskListItems(listed).slice(0, limit);
|
|
9930
|
+
for (const task of tasks) {
|
|
9931
|
+
const taskId = String(task?.id || task?.task_id || task?.taskId || "").trim();
|
|
9932
|
+
if (!taskId) {
|
|
9933
|
+
routed.ignored += 1;
|
|
9934
|
+
continue;
|
|
9935
|
+
}
|
|
9936
|
+
if (ignored.has(normalizeClickUpStatus(clickUpTaskStatus(task)))) {
|
|
9937
|
+
routed.ignored += 1;
|
|
9938
|
+
continue;
|
|
9939
|
+
}
|
|
9940
|
+
if (!isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) {
|
|
9941
|
+
routed.ignored += 1;
|
|
9942
|
+
continue;
|
|
9943
|
+
}
|
|
9944
|
+
try {
|
|
9945
|
+
const result = await routeClickUpWebhookEvent({
|
|
9946
|
+
payload: { webhook_id: mutableState.webhookId || "startup", event: "taskAssigneeUpdated", task_id: taskId, task, startup_reconciliation: true },
|
|
9947
|
+
config,
|
|
9948
|
+
state: mutableState,
|
|
9949
|
+
worktree,
|
|
9950
|
+
clickupClient,
|
|
9951
|
+
openCodeClient,
|
|
9952
|
+
sessionExists,
|
|
9953
|
+
createSession,
|
|
9954
|
+
sendSessionEvent,
|
|
9955
|
+
saveState: persistState,
|
|
9956
|
+
now
|
|
9957
|
+
});
|
|
9958
|
+
if (result?.ok && result.action !== "ignored") routed.assigned += 1;
|
|
9959
|
+
} catch (error) {
|
|
9960
|
+
routed.errors += 1;
|
|
9961
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_failed", taskId, message: error.message });
|
|
9962
|
+
}
|
|
9963
|
+
if (!lastWebhookMs || !clickupClient?.getTaskComments) continue;
|
|
9964
|
+
try {
|
|
9965
|
+
const commentsResponse = await clickupClient.getTaskComments({ taskId, start: lastWebhookMs, limit: CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT });
|
|
9966
|
+
for (const comment of clickUpCommentListItems(commentsResponse).slice(0, CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT)) {
|
|
9967
|
+
const commentMs = clickUpCommentUpdatedMs(comment);
|
|
9968
|
+
if (!commentMs || commentMs <= lastWebhookMs) continue;
|
|
9969
|
+
const authorId = clickUpCommentAuthorId(comment);
|
|
9970
|
+
if (authorId && (authorId === config.routing.ignoredCommentAuthorId || authorId === authorizedUserId)) continue;
|
|
9971
|
+
if (!clickUpCommentMentionsProductManager(comment, config.routing)) continue;
|
|
9972
|
+
const event = comment.date_updated || comment.dateUpdated || comment.updated_at || comment.updatedAt ? "taskCommentUpdated" : "taskCommentPosted";
|
|
9973
|
+
const result = await routeClickUpWebhookEvent({
|
|
9974
|
+
payload: { webhook_id: mutableState.webhookId || "startup", event, task_id: taskId, task, comment, history_item: { id: `startup-${taskId}-${comment.id || commentMs}` }, startup_reconciliation: true },
|
|
9975
|
+
config,
|
|
9976
|
+
state: mutableState,
|
|
9977
|
+
worktree,
|
|
9978
|
+
clickupClient,
|
|
9979
|
+
openCodeClient,
|
|
9980
|
+
sessionExists,
|
|
9981
|
+
createSession,
|
|
9982
|
+
sendSessionEvent,
|
|
9983
|
+
saveState: persistState,
|
|
9984
|
+
now
|
|
9985
|
+
});
|
|
9986
|
+
if (result?.ok && result.action !== "ignored") routed.comments += 1;
|
|
9987
|
+
}
|
|
9988
|
+
} catch (error) {
|
|
9989
|
+
routed.errors += 1;
|
|
9990
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_comments_failed", taskId, message: error.message });
|
|
9991
|
+
}
|
|
9992
|
+
}
|
|
9993
|
+
clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...routed });
|
|
9994
|
+
return { ok: routed.errors === 0, ...routed };
|
|
9995
|
+
}
|
|
9849
9996
|
function clickUpWebhookExpectedPath(config) {
|
|
9850
9997
|
try {
|
|
9851
9998
|
return new URL(config?.webhook?.publicUrl).pathname || "/";
|
|
@@ -9856,9 +10003,14 @@ function clickUpWebhookExpectedPath(config) {
|
|
|
9856
10003
|
async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date() } = {}) {
|
|
9857
10004
|
let payload = null;
|
|
9858
10005
|
let handled = null;
|
|
10006
|
+
let authenticatedWebhook = false;
|
|
10007
|
+
let receivedAt = null;
|
|
9859
10008
|
const finish = (result) => {
|
|
9860
10009
|
handled = result;
|
|
9861
|
-
|
|
10010
|
+
receivedAt ??= now();
|
|
10011
|
+
const auditState = authenticatedWebhook ? { ...state, lastWebhookAt: receivedAt.toISOString() } : state;
|
|
10012
|
+
if (saveState && authenticatedWebhook) saveState(auditState);
|
|
10013
|
+
writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state: auditState, handled, payload, at: receivedAt });
|
|
9862
10014
|
return result;
|
|
9863
10015
|
};
|
|
9864
10016
|
try {
|
|
@@ -9872,12 +10024,15 @@ async function handleClickUpWebhookRequest({ method = "POST", url = null, header
|
|
|
9872
10024
|
if (bodyBytes > maxBodyBytes) return finish({ ok: false, status: 413, reason: "body_too_large" });
|
|
9873
10025
|
const signature = headers["x-signature"] || headers["X-Signature"];
|
|
9874
10026
|
if (!verifyClickUpSignature(rawBody, signature, state?.secret)) return finish({ ok: false, status: 401, reason: "invalid_signature" });
|
|
10027
|
+
authenticatedWebhook = true;
|
|
10028
|
+
receivedAt = now();
|
|
9875
10029
|
try {
|
|
9876
10030
|
payload = JSON.parse(Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : String(rawBody));
|
|
9877
10031
|
} catch {
|
|
9878
10032
|
return finish({ ok: false, status: 400, reason: "invalid_json" });
|
|
9879
10033
|
}
|
|
9880
|
-
const
|
|
10034
|
+
const receivedState = { ...state, lastWebhookAt: receivedAt.toISOString() };
|
|
10035
|
+
const result = await routeClickUpWebhookEvent({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState });
|
|
9881
10036
|
return finish({ ok: result.ok, status: result.ok ? 200 : 422, result });
|
|
9882
10037
|
} catch (error) {
|
|
9883
10038
|
writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state, handled, error, payload, at: now() });
|
|
@@ -11031,6 +11186,18 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
11031
11186
|
listener: { bindHost: clickUpWebhookValidation.config.webhook.bindHost, bindPort: clickUpWebhookValidation.config.webhook.bindPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
11032
11187
|
}, clickUpWebhookValidation.config);
|
|
11033
11188
|
registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry });
|
|
11189
|
+
try {
|
|
11190
|
+
await reconcileClickUpStartup({
|
|
11191
|
+
config: clickUpWebhookValidation.config,
|
|
11192
|
+
state: activeState,
|
|
11193
|
+
worktree,
|
|
11194
|
+
clickupClient: lifecycleClickUpClient,
|
|
11195
|
+
openCodeClient: input.client,
|
|
11196
|
+
saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
|
|
11197
|
+
});
|
|
11198
|
+
} catch (error) {
|
|
11199
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
|
|
11200
|
+
}
|
|
11034
11201
|
} else {
|
|
11035
11202
|
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", reason: readyListener.reason || "listener_unavailable", webhookId: listenerState.webhookId });
|
|
11036
11203
|
markClickUpWebhookInactive(worktree, listenerState, clickUpWebhookValidation.config);
|
|
@@ -11605,7 +11772,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
|
|
|
11605
11772
|
}
|
|
11606
11773
|
};
|
|
11607
11774
|
}
|
|
11608
|
-
OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, createClickUpApiClient, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, readClickUpCommentLedger, readClickUpWebhookState, recordClickUpCommentVersionProcessed, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
|
|
11775
|
+
OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, createClickUpApiClient, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, readClickUpCommentLedger, readClickUpWebhookState, reconcileClickUpStartup, recordClickUpCommentVersionProcessed, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
|
|
11609
11776
|
export {
|
|
11610
11777
|
OptimaPlugin as default
|
|
11611
11778
|
};
|
package/dist/sanitize_cli.js
CHANGED
|
@@ -7942,6 +7942,8 @@ var CLICKUP_PM_MENTION_NAME = "Defend Tech Product Manager";
|
|
|
7942
7942
|
var CLICKUP_WEBHOOK_RUNTIME_VERSION = 1;
|
|
7943
7943
|
var CLICKUP_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
7944
7944
|
var CLICKUP_WEBHOOK_REQUEST_TIMEOUT_MS = 1e4;
|
|
7945
|
+
var CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT = 50;
|
|
7946
|
+
var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
|
|
7945
7947
|
var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
|
|
7946
7948
|
var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
|
|
7947
7949
|
var DISCUSSION_BACKFILL_FETCH_LIMIT = 100;
|
|
@@ -9060,6 +9062,7 @@ function sanitizeClickUpWebhookState(state = {}, config = null) {
|
|
|
9060
9062
|
lastValidatedAt: state.lastValidatedAt || null,
|
|
9061
9063
|
listener: isPlainObject(state.listener) ? state.listener : {},
|
|
9062
9064
|
pendingSessions: isPlainObject(state.pendingSessions) ? state.pendingSessions : {},
|
|
9065
|
+
lastWebhookAt: state.lastWebhookAt || state.last_webhook_at || null,
|
|
9063
9066
|
recentEventKeys
|
|
9064
9067
|
};
|
|
9065
9068
|
}
|
|
@@ -9142,6 +9145,9 @@ function createClickUpApiClient(config, fetchImpl = globalThis.fetch) {
|
|
|
9142
9145
|
async deleteWebhook(webhookId) {
|
|
9143
9146
|
return request(`https://api.clickup.com/api/v2/webhook/${encodeURIComponent(webhookId)}`, { method: "DELETE" });
|
|
9144
9147
|
},
|
|
9148
|
+
async getAuthorizedUser() {
|
|
9149
|
+
return request("https://api.clickup.com/api/v2/user");
|
|
9150
|
+
},
|
|
9145
9151
|
async getTask(taskId) {
|
|
9146
9152
|
return request(`https://api.clickup.com/api/v2/task/${encodeURIComponent(taskId)}`);
|
|
9147
9153
|
},
|
|
@@ -9156,6 +9162,20 @@ function createClickUpApiClient(config, fetchImpl = globalThis.fetch) {
|
|
|
9156
9162
|
method: "POST",
|
|
9157
9163
|
body: JSON.stringify({ comment_text: comment })
|
|
9158
9164
|
});
|
|
9165
|
+
},
|
|
9166
|
+
async listAssignedTasks({ assigneeId, statuses = [], limit = CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT } = {}) {
|
|
9167
|
+
const params = new URLSearchParams();
|
|
9168
|
+
if (assigneeId) params.append("assignees[]", assigneeId);
|
|
9169
|
+
for (const status of statuses) if (status) params.append("statuses[]", status);
|
|
9170
|
+
if (limit) params.append("limit", String(limit));
|
|
9171
|
+
return request(`https://api.clickup.com/api/v2/team/${encodeURIComponent(config.teamId)}/task?${params.toString()}`);
|
|
9172
|
+
},
|
|
9173
|
+
async getTaskComments({ taskId, start, limit = CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT } = {}) {
|
|
9174
|
+
const params = new URLSearchParams();
|
|
9175
|
+
if (start) params.set("start", String(start));
|
|
9176
|
+
if (limit) params.set("limit", String(limit));
|
|
9177
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
9178
|
+
return request(`https://api.clickup.com/api/v2/task/${encodeURIComponent(taskId)}/comment${suffix}`);
|
|
9159
9179
|
}
|
|
9160
9180
|
};
|
|
9161
9181
|
}
|
|
@@ -9201,7 +9221,7 @@ async function findReusableClickUpWebhook(config, clickupClient = null) {
|
|
|
9201
9221
|
}
|
|
9202
9222
|
return null;
|
|
9203
9223
|
}
|
|
9204
|
-
async function validateClickUpWebhookState(state, config, clickupClient = null) {
|
|
9224
|
+
async function validateClickUpWebhookState(state, config, clickupClient = null, { allowRemoteUnhealthyLocalRecovery = false } = {}) {
|
|
9205
9225
|
if (!isClickUpWebhookStateActive(state, config)) return { valid: false, reason: "state_incomplete" };
|
|
9206
9226
|
if (clickupClient?.listWebhooks) {
|
|
9207
9227
|
const listed = await clickupClient.listWebhooks({ teamId: config.teamId });
|
|
@@ -9209,10 +9229,22 @@ async function validateClickUpWebhookState(state, config, clickupClient = null)
|
|
|
9209
9229
|
if (!match) return { valid: false, reason: "remote_state_missing" };
|
|
9210
9230
|
const remote = normalizeClickUpWebhookApiResponse(match, config);
|
|
9211
9231
|
const remoteWithLocalSecret = { ...remote, secret: state.secret };
|
|
9212
|
-
if (
|
|
9213
|
-
return { valid:
|
|
9232
|
+
if (isClickUpWebhookStateActive(remoteWithLocalSecret, config) && remote.webhookId === state.webhookId) {
|
|
9233
|
+
return { valid: true, mode: "remote", state: { ...remoteWithLocalSecret, recentEventKeys: state.recentEventKeys || [], lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
9234
|
+
}
|
|
9235
|
+
const localStateValidation = await validateClickUpWebhookState(state, config, null);
|
|
9236
|
+
if (allowRemoteUnhealthyLocalRecovery && localStateValidation.valid && remote.webhookId === state.webhookId) {
|
|
9237
|
+
const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && [...new Set(config.webhook.events || [])].every((event) => new Set(remote.events || []).has(event));
|
|
9238
|
+
if (remoteConfigMatches) {
|
|
9239
|
+
return {
|
|
9240
|
+
...localStateValidation,
|
|
9241
|
+
mode: "local_state_remote_unhealthy",
|
|
9242
|
+
limitation: "ClickUp remote webhook is present but unhealthy; Optima starts the local listener from matching local id/secret/config so delivery can recover.",
|
|
9243
|
+
remote
|
|
9244
|
+
};
|
|
9245
|
+
}
|
|
9214
9246
|
}
|
|
9215
|
-
return { valid:
|
|
9247
|
+
return { valid: false, reason: "remote_state_mismatch", remote };
|
|
9216
9248
|
}
|
|
9217
9249
|
return { valid: true, mode: "local_state", state: { ...state, lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString() }, limitation: "ClickUp API validation unavailable; Optima trusts ignored local runtime state only after id/secret/config checks." };
|
|
9218
9250
|
}
|
|
@@ -9222,7 +9254,7 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
|
|
|
9222
9254
|
}
|
|
9223
9255
|
const { config } = validation;
|
|
9224
9256
|
const existing = readClickUpWebhookState(worktree, config);
|
|
9225
|
-
const existingValidation = await validateClickUpWebhookState(existing, config, clickupClient);
|
|
9257
|
+
const existingValidation = await validateClickUpWebhookState(existing, config, clickupClient, { allowRemoteUnhealthyLocalRecovery: true });
|
|
9226
9258
|
if (existingValidation.valid) {
|
|
9227
9259
|
const state = writeClickUpWebhookState(worktree, { ...existingValidation.state, recentEventKeys: existing.recentEventKeys || [] }, config);
|
|
9228
9260
|
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: existingValidation.mode });
|
|
@@ -9373,6 +9405,31 @@ function recordClickUpCommentVersionProcessed({ ledgerPath, key, taskId, eventTy
|
|
|
9373
9405
|
function clickUpTaskIdFromPayload(payload = {}) {
|
|
9374
9406
|
return String(payload.task_id || payload.taskId || payload.task?.id || payload.task?.task_id || "").trim();
|
|
9375
9407
|
}
|
|
9408
|
+
function clickUpTaskListItems(response = {}) {
|
|
9409
|
+
if (Array.isArray(response)) return response;
|
|
9410
|
+
if (Array.isArray(response.tasks)) return response.tasks;
|
|
9411
|
+
if (Array.isArray(response.data)) return response.data;
|
|
9412
|
+
if (Array.isArray(response?.data?.tasks)) return response.data.tasks;
|
|
9413
|
+
return [];
|
|
9414
|
+
}
|
|
9415
|
+
function clickUpCommentListItems(response = {}) {
|
|
9416
|
+
if (Array.isArray(response)) return response;
|
|
9417
|
+
if (Array.isArray(response.comments)) return response.comments;
|
|
9418
|
+
if (Array.isArray(response.data)) return response.data;
|
|
9419
|
+
if (Array.isArray(response?.data?.comments)) return response.data.comments;
|
|
9420
|
+
return [];
|
|
9421
|
+
}
|
|
9422
|
+
function clickUpTimestampMs(value) {
|
|
9423
|
+
if (value === null || value === void 0 || value === "") return 0;
|
|
9424
|
+
if (value instanceof Date) return value.getTime();
|
|
9425
|
+
const numeric = Number(value);
|
|
9426
|
+
if (Number.isFinite(numeric) && numeric > 0) return numeric < 1e10 ? numeric * 1e3 : numeric;
|
|
9427
|
+
const parsed = Date.parse(String(value));
|
|
9428
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
9429
|
+
}
|
|
9430
|
+
function clickUpCommentUpdatedMs(comment = {}) {
|
|
9431
|
+
return clickUpTimestampMs(comment.date_updated || comment.dateUpdated || comment.updated_at || comment.updatedAt || comment.date || comment.date_created || comment.created_at);
|
|
9432
|
+
}
|
|
9376
9433
|
function clickUpTaskName(task = {}) {
|
|
9377
9434
|
return String(task?.name || "").trim();
|
|
9378
9435
|
}
|
|
@@ -9765,12 +9822,18 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
|
|
|
9765
9822
|
if (!taskId) return { ok: false, action: "error", reason: "missing_task_id" };
|
|
9766
9823
|
let task = payload.task || (clickupClient?.getTask ? await clickupClient.getTask(taskId) : null);
|
|
9767
9824
|
if (!task) return { ok: false, action: "error", reason: "task_unavailable", taskId };
|
|
9825
|
+
if (clickupClient?.getTask) {
|
|
9826
|
+
const latestTask = await clickupClient.getTask(taskId);
|
|
9827
|
+
if (latestTask) task = latestTask;
|
|
9828
|
+
}
|
|
9768
9829
|
const isCommentEvent = eventType === "taskCommentPosted" || eventType === "taskCommentUpdated";
|
|
9769
9830
|
if (isCommentEvent) {
|
|
9770
9831
|
const comment = clickUpCommentFromPayload(payload);
|
|
9771
9832
|
const authorId = clickUpCommentAuthorId(comment);
|
|
9772
9833
|
if (authorId && authorId === config.routing.ignoredCommentAuthorId) return finish({ ok: true, action: "ignored", reason: "self_authored_comment", taskId });
|
|
9773
|
-
|
|
9834
|
+
const mentionsProductManager = clickUpCommentMentionsProductManager(comment, config.routing);
|
|
9835
|
+
const assignedToProductManager = isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId);
|
|
9836
|
+
if (!mentionsProductManager && !assignedToProductManager) return finish({ ok: true, action: "ignored", reason: "missing_product_manager_mention_or_assignment", taskId });
|
|
9774
9837
|
commentLedgerKey = clickUpCommentLedgerKey({ taskId, eventType, payload });
|
|
9775
9838
|
try {
|
|
9776
9839
|
if (isClickUpCommentVersionProcessed({ ledgerPath: commentLedgerPath, key: commentLedgerKey, worktree })) {
|
|
@@ -9782,10 +9845,6 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
|
|
|
9782
9845
|
}
|
|
9783
9846
|
if (!isCommentEvent && !isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) return finish({ ok: true, action: "ignored", reason: "not_assigned_to_product_manager", taskId });
|
|
9784
9847
|
if (isClickUpTaskTerminal(task, payload, config.routing.ignoredStatuses)) return finish({ ok: true, action: "ignored", reason: "terminal_status", taskId });
|
|
9785
|
-
if (clickupClient?.getTask) {
|
|
9786
|
-
const latestTask = await clickupClient.getTask(taskId);
|
|
9787
|
-
if (latestTask) task = latestTask;
|
|
9788
|
-
}
|
|
9789
9848
|
const sessionTitle = formatClickUpSessionTitle({ taskId, payloadTask: payload.task, task });
|
|
9790
9849
|
const existingMetadata = clickUpTaskAgentMetadata(task, config.routing.metadataFieldId);
|
|
9791
9850
|
const metadata = normalizeAgentMetadataJson(existingMetadata);
|
|
@@ -9853,6 +9912,94 @@ async function routeClickUpWebhookEvent(options = {}) {
|
|
|
9853
9912
|
const taskId = clickUpTaskIdFromPayload(options.payload || {});
|
|
9854
9913
|
return withClickUpTaskRouteLock(taskId, () => routeClickUpWebhookEventUnlocked(options));
|
|
9855
9914
|
}
|
|
9915
|
+
async function reconcileClickUpStartup({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, saveState = null, now = () => /* @__PURE__ */ new Date(), limit = CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT } = {}) {
|
|
9916
|
+
if (!config || !clickupClient?.listAssignedTasks) return { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
|
|
9917
|
+
let authorizedUserId = "";
|
|
9918
|
+
if (clickupClient?.getAuthorizedUser) {
|
|
9919
|
+
try {
|
|
9920
|
+
const userResponse = await clickupClient.getAuthorizedUser();
|
|
9921
|
+
authorizedUserId = String(userResponse?.user?.id || userResponse?.id || "").trim();
|
|
9922
|
+
} catch (error) {
|
|
9923
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_user_lookup_failed", message: error.message });
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9926
|
+
let mutableState = state;
|
|
9927
|
+
const persistState = (nextState) => {
|
|
9928
|
+
mutableState = nextState;
|
|
9929
|
+
if (saveState) saveState(nextState);
|
|
9930
|
+
};
|
|
9931
|
+
const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
|
|
9932
|
+
const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
|
|
9933
|
+
const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0 };
|
|
9934
|
+
clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
|
|
9935
|
+
const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
|
|
9936
|
+
const tasks = clickUpTaskListItems(listed).slice(0, limit);
|
|
9937
|
+
for (const task of tasks) {
|
|
9938
|
+
const taskId = String(task?.id || task?.task_id || task?.taskId || "").trim();
|
|
9939
|
+
if (!taskId) {
|
|
9940
|
+
routed.ignored += 1;
|
|
9941
|
+
continue;
|
|
9942
|
+
}
|
|
9943
|
+
if (ignored.has(normalizeClickUpStatus(clickUpTaskStatus(task)))) {
|
|
9944
|
+
routed.ignored += 1;
|
|
9945
|
+
continue;
|
|
9946
|
+
}
|
|
9947
|
+
if (!isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) {
|
|
9948
|
+
routed.ignored += 1;
|
|
9949
|
+
continue;
|
|
9950
|
+
}
|
|
9951
|
+
try {
|
|
9952
|
+
const result = await routeClickUpWebhookEvent({
|
|
9953
|
+
payload: { webhook_id: mutableState.webhookId || "startup", event: "taskAssigneeUpdated", task_id: taskId, task, startup_reconciliation: true },
|
|
9954
|
+
config,
|
|
9955
|
+
state: mutableState,
|
|
9956
|
+
worktree,
|
|
9957
|
+
clickupClient,
|
|
9958
|
+
openCodeClient,
|
|
9959
|
+
sessionExists,
|
|
9960
|
+
createSession,
|
|
9961
|
+
sendSessionEvent,
|
|
9962
|
+
saveState: persistState,
|
|
9963
|
+
now
|
|
9964
|
+
});
|
|
9965
|
+
if (result?.ok && result.action !== "ignored") routed.assigned += 1;
|
|
9966
|
+
} catch (error) {
|
|
9967
|
+
routed.errors += 1;
|
|
9968
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_failed", taskId, message: error.message });
|
|
9969
|
+
}
|
|
9970
|
+
if (!lastWebhookMs || !clickupClient?.getTaskComments) continue;
|
|
9971
|
+
try {
|
|
9972
|
+
const commentsResponse = await clickupClient.getTaskComments({ taskId, start: lastWebhookMs, limit: CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT });
|
|
9973
|
+
for (const comment of clickUpCommentListItems(commentsResponse).slice(0, CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT)) {
|
|
9974
|
+
const commentMs = clickUpCommentUpdatedMs(comment);
|
|
9975
|
+
if (!commentMs || commentMs <= lastWebhookMs) continue;
|
|
9976
|
+
const authorId = clickUpCommentAuthorId(comment);
|
|
9977
|
+
if (authorId && (authorId === config.routing.ignoredCommentAuthorId || authorId === authorizedUserId)) continue;
|
|
9978
|
+
if (!clickUpCommentMentionsProductManager(comment, config.routing)) continue;
|
|
9979
|
+
const event = comment.date_updated || comment.dateUpdated || comment.updated_at || comment.updatedAt ? "taskCommentUpdated" : "taskCommentPosted";
|
|
9980
|
+
const result = await routeClickUpWebhookEvent({
|
|
9981
|
+
payload: { webhook_id: mutableState.webhookId || "startup", event, task_id: taskId, task, comment, history_item: { id: `startup-${taskId}-${comment.id || commentMs}` }, startup_reconciliation: true },
|
|
9982
|
+
config,
|
|
9983
|
+
state: mutableState,
|
|
9984
|
+
worktree,
|
|
9985
|
+
clickupClient,
|
|
9986
|
+
openCodeClient,
|
|
9987
|
+
sessionExists,
|
|
9988
|
+
createSession,
|
|
9989
|
+
sendSessionEvent,
|
|
9990
|
+
saveState: persistState,
|
|
9991
|
+
now
|
|
9992
|
+
});
|
|
9993
|
+
if (result?.ok && result.action !== "ignored") routed.comments += 1;
|
|
9994
|
+
}
|
|
9995
|
+
} catch (error) {
|
|
9996
|
+
routed.errors += 1;
|
|
9997
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_comments_failed", taskId, message: error.message });
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...routed });
|
|
10001
|
+
return { ok: routed.errors === 0, ...routed };
|
|
10002
|
+
}
|
|
9856
10003
|
function clickUpWebhookExpectedPath(config) {
|
|
9857
10004
|
try {
|
|
9858
10005
|
return new URL(config?.webhook?.publicUrl).pathname || "/";
|
|
@@ -9863,9 +10010,14 @@ function clickUpWebhookExpectedPath(config) {
|
|
|
9863
10010
|
async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date() } = {}) {
|
|
9864
10011
|
let payload = null;
|
|
9865
10012
|
let handled = null;
|
|
10013
|
+
let authenticatedWebhook = false;
|
|
10014
|
+
let receivedAt = null;
|
|
9866
10015
|
const finish = (result) => {
|
|
9867
10016
|
handled = result;
|
|
9868
|
-
|
|
10017
|
+
receivedAt ??= now();
|
|
10018
|
+
const auditState = authenticatedWebhook ? { ...state, lastWebhookAt: receivedAt.toISOString() } : state;
|
|
10019
|
+
if (saveState && authenticatedWebhook) saveState(auditState);
|
|
10020
|
+
writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state: auditState, handled, payload, at: receivedAt });
|
|
9869
10021
|
return result;
|
|
9870
10022
|
};
|
|
9871
10023
|
try {
|
|
@@ -9879,12 +10031,15 @@ async function handleClickUpWebhookRequest({ method = "POST", url = null, header
|
|
|
9879
10031
|
if (bodyBytes > maxBodyBytes) return finish({ ok: false, status: 413, reason: "body_too_large" });
|
|
9880
10032
|
const signature = headers["x-signature"] || headers["X-Signature"];
|
|
9881
10033
|
if (!verifyClickUpSignature(rawBody, signature, state?.secret)) return finish({ ok: false, status: 401, reason: "invalid_signature" });
|
|
10034
|
+
authenticatedWebhook = true;
|
|
10035
|
+
receivedAt = now();
|
|
9882
10036
|
try {
|
|
9883
10037
|
payload = JSON.parse(Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : String(rawBody));
|
|
9884
10038
|
} catch {
|
|
9885
10039
|
return finish({ ok: false, status: 400, reason: "invalid_json" });
|
|
9886
10040
|
}
|
|
9887
|
-
const
|
|
10041
|
+
const receivedState = { ...state, lastWebhookAt: receivedAt.toISOString() };
|
|
10042
|
+
const result = await routeClickUpWebhookEvent({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState });
|
|
9888
10043
|
return finish({ ok: result.ok, status: result.ok ? 200 : 422, result });
|
|
9889
10044
|
} catch (error) {
|
|
9890
10045
|
writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state, handled, error, payload, at: now() });
|
|
@@ -11038,6 +11193,18 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
11038
11193
|
listener: { bindHost: clickUpWebhookValidation.config.webhook.bindHost, bindPort: clickUpWebhookValidation.config.webhook.bindPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
11039
11194
|
}, clickUpWebhookValidation.config);
|
|
11040
11195
|
registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry });
|
|
11196
|
+
try {
|
|
11197
|
+
await reconcileClickUpStartup({
|
|
11198
|
+
config: clickUpWebhookValidation.config,
|
|
11199
|
+
state: activeState,
|
|
11200
|
+
worktree,
|
|
11201
|
+
clickupClient: lifecycleClickUpClient,
|
|
11202
|
+
openCodeClient: input.client,
|
|
11203
|
+
saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
|
|
11204
|
+
});
|
|
11205
|
+
} catch (error) {
|
|
11206
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
|
|
11207
|
+
}
|
|
11041
11208
|
} else {
|
|
11042
11209
|
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", reason: readyListener.reason || "listener_unavailable", webhookId: listenerState.webhookId });
|
|
11043
11210
|
markClickUpWebhookInactive(worktree, listenerState, clickUpWebhookValidation.config);
|
|
@@ -11612,7 +11779,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
|
|
|
11612
11779
|
}
|
|
11613
11780
|
};
|
|
11614
11781
|
}
|
|
11615
|
-
OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, createClickUpApiClient, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, readClickUpCommentLedger, readClickUpWebhookState, recordClickUpCommentVersionProcessed, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
|
|
11782
|
+
OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, createClickUpApiClient, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, readClickUpCommentLedger, readClickUpWebhookState, reconcileClickUpStartup, recordClickUpCommentVersionProcessed, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
|
|
11616
11783
|
|
|
11617
11784
|
// src/sanitize_cli.js
|
|
11618
11785
|
var { migrateLegacyOptimaLayout: migrateLegacyOptimaLayout2 } = OptimaPlugin.__internals;
|