@defend-tech/opencode-optima 0.1.35 → 0.1.37
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 +214 -6
- package/dist/sanitize_cli.js +214 -6
- 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
|
}
|
|
@@ -9378,6 +9398,31 @@ function recordClickUpCommentVersionProcessed({ ledgerPath, key, taskId, eventTy
|
|
|
9378
9398
|
function clickUpTaskIdFromPayload(payload = {}) {
|
|
9379
9399
|
return String(payload.task_id || payload.taskId || payload.task?.id || payload.task?.task_id || "").trim();
|
|
9380
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
|
+
}
|
|
9381
9426
|
function clickUpTaskName(task = {}) {
|
|
9382
9427
|
return String(task?.name || "").trim();
|
|
9383
9428
|
}
|
|
@@ -9571,6 +9616,45 @@ function formatClickUpWebhookPrompt({ eventType, taskId, payload, branch = "", w
|
|
|
9571
9616
|
deliveryEvidencePath ? `Final merge-trackable evidence must be written under ${deliveryEvidencePath}; use .optima only as local mirror/staging.` : null
|
|
9572
9617
|
].filter((part) => part !== null).join("\n");
|
|
9573
9618
|
}
|
|
9619
|
+
function clickUpNoAbandonmentRuleText() {
|
|
9620
|
+
return "No-abandonment rule: PM must keep working, or before stopping must post a ClickUp comment, hand off, remove PM as assignee, and assign the next owner.";
|
|
9621
|
+
}
|
|
9622
|
+
function buildClickUpStartupResumeComment({ taskId, result = {} } = {}) {
|
|
9623
|
+
const contextParts = [
|
|
9624
|
+
result.branch ? `- Branch: ${result.branch}` : null,
|
|
9625
|
+
result.worktree ? `- Worktree: ${result.worktree}` : null,
|
|
9626
|
+
result.deliveryEvidencePath ? `- Delivery evidence path: ${result.deliveryEvidencePath}` : null,
|
|
9627
|
+
result.evidencePath ? `- Local evidence path: ${result.evidencePath}` : null
|
|
9628
|
+
].filter(Boolean);
|
|
9629
|
+
return [
|
|
9630
|
+
`Startup reconciliation resumed ClickUp task ${taskId}.`,
|
|
9631
|
+
`Route result: ${result.action || "unknown"}${result.ok === false ? " (failed)" : ""}.`,
|
|
9632
|
+
result.sessionId ? `Session id: ${result.sessionId}.` : "Session id: unavailable.",
|
|
9633
|
+
contextParts.length ? contextParts.join("\n") : "Worktree/branch/delivery evidence path: unavailable.",
|
|
9634
|
+
clickUpNoAbandonmentRuleText()
|
|
9635
|
+
].join("\n");
|
|
9636
|
+
}
|
|
9637
|
+
function buildClickUpStartupBlockerComment({ taskId, error } = {}) {
|
|
9638
|
+
const summary = error?.message || String(error || "unknown error");
|
|
9639
|
+
return [
|
|
9640
|
+
`Startup reconciliation blocker for ClickUp task ${taskId}: ${summary}`,
|
|
9641
|
+
"Optima skipped this task for this startup pass and continued with the next PM-assigned task.",
|
|
9642
|
+
clickUpNoAbandonmentRuleText()
|
|
9643
|
+
].join("\n");
|
|
9644
|
+
}
|
|
9645
|
+
async function postClickUpStartupComment({ clickupClient, worktree, taskId, comment, type } = {}) {
|
|
9646
|
+
if (typeof clickupClient?.postTaskComment !== "function") {
|
|
9647
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_comment_unavailable", taskId, commentType: type });
|
|
9648
|
+
return { ok: false, skipped: true, reason: "post_task_comment_unavailable" };
|
|
9649
|
+
}
|
|
9650
|
+
try {
|
|
9651
|
+
await clickupClient.postTaskComment({ taskId, comment });
|
|
9652
|
+
return { ok: true };
|
|
9653
|
+
} catch (error) {
|
|
9654
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_comment_failed", taskId, commentType: type, message: error.message });
|
|
9655
|
+
return { ok: false, error: error.message };
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9574
9658
|
function appendClickUpWebhookLocalLog(worktree, entry) {
|
|
9575
9659
|
const logPath = clickUpWebhookLogPath(worktree);
|
|
9576
9660
|
fs2.mkdirSync(path2.dirname(logPath), { recursive: true });
|
|
@@ -9842,24 +9926,128 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
|
|
|
9842
9926
|
await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: nextMetadata });
|
|
9843
9927
|
const { [taskId]: _completedPending, ...remainingPending } = stateToPersist.pendingSessions || {};
|
|
9844
9928
|
stateToPersist = { ...stateToPersist, pendingSessions: remainingPending };
|
|
9845
|
-
return finish({ ok: true, action: "created_session", taskId, sessionId: pendingSessionId, eventKey });
|
|
9929
|
+
return finish({ ok: true, action: "created_session", taskId, sessionId: pendingSessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path });
|
|
9846
9930
|
}
|
|
9847
9931
|
const sessionId = String(existingSessionId);
|
|
9848
9932
|
if (await sessionExists(openCodeClient, sessionId)) {
|
|
9849
9933
|
if (typeof clickupClient?.updateTaskMetadata === "function") await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: metadataWithRouting });
|
|
9850
9934
|
await sendSessionEvent(openCodeClient, { sessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl });
|
|
9851
|
-
return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey });
|
|
9935
|
+
return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path });
|
|
9852
9936
|
}
|
|
9853
9937
|
const at = now().toISOString();
|
|
9854
9938
|
const incidentComment = `Optima webhook could not route this ClickUp event because OpenCode session ${sessionId} is missing on host ${host} at ${at}. No replacement session was created automatically.`;
|
|
9855
9939
|
appendClickUpWebhookLocalLog(worktree, { type: "missing_session", taskId, sessionId, host, at });
|
|
9856
9940
|
await clickupClient.postTaskComment({ taskId, comment: incidentComment });
|
|
9857
|
-
return finish({ ok: true, action: "missing_session_reported", taskId, sessionId, eventKey });
|
|
9941
|
+
return finish({ ok: true, action: "missing_session_reported", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path });
|
|
9858
9942
|
}
|
|
9859
9943
|
async function routeClickUpWebhookEvent(options = {}) {
|
|
9860
9944
|
const taskId = clickUpTaskIdFromPayload(options.payload || {});
|
|
9861
9945
|
return withClickUpTaskRouteLock(taskId, () => routeClickUpWebhookEventUnlocked(options));
|
|
9862
9946
|
}
|
|
9947
|
+
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 } = {}) {
|
|
9948
|
+
if (!config || !clickupClient?.listAssignedTasks) return { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
|
|
9949
|
+
let authorizedUserId = "";
|
|
9950
|
+
if (clickupClient?.getAuthorizedUser) {
|
|
9951
|
+
try {
|
|
9952
|
+
const userResponse = await clickupClient.getAuthorizedUser();
|
|
9953
|
+
authorizedUserId = String(userResponse?.user?.id || userResponse?.id || "").trim();
|
|
9954
|
+
} catch (error) {
|
|
9955
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_user_lookup_failed", message: error.message });
|
|
9956
|
+
}
|
|
9957
|
+
}
|
|
9958
|
+
let mutableState = state;
|
|
9959
|
+
const persistState = (nextState) => {
|
|
9960
|
+
mutableState = nextState;
|
|
9961
|
+
if (saveState) saveState(nextState);
|
|
9962
|
+
};
|
|
9963
|
+
const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
|
|
9964
|
+
const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
|
|
9965
|
+
const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0 };
|
|
9966
|
+
clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
|
|
9967
|
+
const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
|
|
9968
|
+
const tasks = clickUpTaskListItems(listed).slice(0, limit);
|
|
9969
|
+
for (const task of tasks) {
|
|
9970
|
+
const taskId = String(task?.id || task?.task_id || task?.taskId || "").trim();
|
|
9971
|
+
if (!taskId) {
|
|
9972
|
+
routed.ignored += 1;
|
|
9973
|
+
continue;
|
|
9974
|
+
}
|
|
9975
|
+
if (ignored.has(normalizeClickUpStatus(clickUpTaskStatus(task)))) {
|
|
9976
|
+
routed.ignored += 1;
|
|
9977
|
+
continue;
|
|
9978
|
+
}
|
|
9979
|
+
if (!isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) {
|
|
9980
|
+
routed.ignored += 1;
|
|
9981
|
+
continue;
|
|
9982
|
+
}
|
|
9983
|
+
try {
|
|
9984
|
+
const result = await routeClickUpWebhookEvent({
|
|
9985
|
+
payload: { webhook_id: mutableState.webhookId || "startup", event: "taskAssigneeUpdated", task_id: taskId, task, startup_reconciliation: true },
|
|
9986
|
+
config,
|
|
9987
|
+
state: mutableState,
|
|
9988
|
+
worktree,
|
|
9989
|
+
clickupClient,
|
|
9990
|
+
openCodeClient,
|
|
9991
|
+
sessionExists,
|
|
9992
|
+
createSession,
|
|
9993
|
+
sendSessionEvent,
|
|
9994
|
+
saveState: persistState,
|
|
9995
|
+
now
|
|
9996
|
+
});
|
|
9997
|
+
if (result?.ok && result.action !== "ignored") {
|
|
9998
|
+
routed.assigned += 1;
|
|
9999
|
+
await postClickUpStartupComment({
|
|
10000
|
+
clickupClient,
|
|
10001
|
+
worktree,
|
|
10002
|
+
taskId,
|
|
10003
|
+
type: "resume",
|
|
10004
|
+
comment: buildClickUpStartupResumeComment({ taskId, result })
|
|
10005
|
+
});
|
|
10006
|
+
}
|
|
10007
|
+
} catch (error) {
|
|
10008
|
+
routed.errors += 1;
|
|
10009
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_failed", taskId, message: error.message });
|
|
10010
|
+
await postClickUpStartupComment({
|
|
10011
|
+
clickupClient,
|
|
10012
|
+
worktree,
|
|
10013
|
+
taskId,
|
|
10014
|
+
type: "blocker",
|
|
10015
|
+
comment: buildClickUpStartupBlockerComment({ taskId, error })
|
|
10016
|
+
});
|
|
10017
|
+
}
|
|
10018
|
+
if (!lastWebhookMs || !clickupClient?.getTaskComments) continue;
|
|
10019
|
+
try {
|
|
10020
|
+
const commentsResponse = await clickupClient.getTaskComments({ taskId, start: lastWebhookMs, limit: CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT });
|
|
10021
|
+
for (const comment of clickUpCommentListItems(commentsResponse).slice(0, CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT)) {
|
|
10022
|
+
const commentMs = clickUpCommentUpdatedMs(comment);
|
|
10023
|
+
if (!commentMs || commentMs <= lastWebhookMs) continue;
|
|
10024
|
+
const authorId = clickUpCommentAuthorId(comment);
|
|
10025
|
+
if (authorId && (authorId === config.routing.ignoredCommentAuthorId || authorId === authorizedUserId)) continue;
|
|
10026
|
+
if (!clickUpCommentMentionsProductManager(comment, config.routing)) continue;
|
|
10027
|
+
const event = comment.date_updated || comment.dateUpdated || comment.updated_at || comment.updatedAt ? "taskCommentUpdated" : "taskCommentPosted";
|
|
10028
|
+
const result = await routeClickUpWebhookEvent({
|
|
10029
|
+
payload: { webhook_id: mutableState.webhookId || "startup", event, task_id: taskId, task, comment, history_item: { id: `startup-${taskId}-${comment.id || commentMs}` }, startup_reconciliation: true },
|
|
10030
|
+
config,
|
|
10031
|
+
state: mutableState,
|
|
10032
|
+
worktree,
|
|
10033
|
+
clickupClient,
|
|
10034
|
+
openCodeClient,
|
|
10035
|
+
sessionExists,
|
|
10036
|
+
createSession,
|
|
10037
|
+
sendSessionEvent,
|
|
10038
|
+
saveState: persistState,
|
|
10039
|
+
now
|
|
10040
|
+
});
|
|
10041
|
+
if (result?.ok && result.action !== "ignored") routed.comments += 1;
|
|
10042
|
+
}
|
|
10043
|
+
} catch (error) {
|
|
10044
|
+
routed.errors += 1;
|
|
10045
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_comments_failed", taskId, message: error.message });
|
|
10046
|
+
}
|
|
10047
|
+
}
|
|
10048
|
+
clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...routed });
|
|
10049
|
+
return { ok: routed.errors === 0, ...routed };
|
|
10050
|
+
}
|
|
9863
10051
|
function clickUpWebhookExpectedPath(config) {
|
|
9864
10052
|
try {
|
|
9865
10053
|
return new URL(config?.webhook?.publicUrl).pathname || "/";
|
|
@@ -9870,9 +10058,14 @@ function clickUpWebhookExpectedPath(config) {
|
|
|
9870
10058
|
async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date() } = {}) {
|
|
9871
10059
|
let payload = null;
|
|
9872
10060
|
let handled = null;
|
|
10061
|
+
let authenticatedWebhook = false;
|
|
10062
|
+
let receivedAt = null;
|
|
9873
10063
|
const finish = (result) => {
|
|
9874
10064
|
handled = result;
|
|
9875
|
-
|
|
10065
|
+
receivedAt ??= now();
|
|
10066
|
+
const auditState = authenticatedWebhook ? { ...state, lastWebhookAt: receivedAt.toISOString() } : state;
|
|
10067
|
+
if (saveState && authenticatedWebhook) saveState(auditState);
|
|
10068
|
+
writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state: auditState, handled, payload, at: receivedAt });
|
|
9876
10069
|
return result;
|
|
9877
10070
|
};
|
|
9878
10071
|
try {
|
|
@@ -9886,12 +10079,15 @@ async function handleClickUpWebhookRequest({ method = "POST", url = null, header
|
|
|
9886
10079
|
if (bodyBytes > maxBodyBytes) return finish({ ok: false, status: 413, reason: "body_too_large" });
|
|
9887
10080
|
const signature = headers["x-signature"] || headers["X-Signature"];
|
|
9888
10081
|
if (!verifyClickUpSignature(rawBody, signature, state?.secret)) return finish({ ok: false, status: 401, reason: "invalid_signature" });
|
|
10082
|
+
authenticatedWebhook = true;
|
|
10083
|
+
receivedAt = now();
|
|
9889
10084
|
try {
|
|
9890
10085
|
payload = JSON.parse(Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : String(rawBody));
|
|
9891
10086
|
} catch {
|
|
9892
10087
|
return finish({ ok: false, status: 400, reason: "invalid_json" });
|
|
9893
10088
|
}
|
|
9894
|
-
const
|
|
10089
|
+
const receivedState = { ...state, lastWebhookAt: receivedAt.toISOString() };
|
|
10090
|
+
const result = await routeClickUpWebhookEvent({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState });
|
|
9895
10091
|
return finish({ ok: result.ok, status: result.ok ? 200 : 422, result });
|
|
9896
10092
|
} catch (error) {
|
|
9897
10093
|
writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state, handled, error, payload, at: now() });
|
|
@@ -11045,6 +11241,18 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
11045
11241
|
listener: { bindHost: clickUpWebhookValidation.config.webhook.bindHost, bindPort: clickUpWebhookValidation.config.webhook.bindPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
11046
11242
|
}, clickUpWebhookValidation.config);
|
|
11047
11243
|
registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry });
|
|
11244
|
+
try {
|
|
11245
|
+
await reconcileClickUpStartup({
|
|
11246
|
+
config: clickUpWebhookValidation.config,
|
|
11247
|
+
state: activeState,
|
|
11248
|
+
worktree,
|
|
11249
|
+
clickupClient: lifecycleClickUpClient,
|
|
11250
|
+
openCodeClient: input.client,
|
|
11251
|
+
saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
|
|
11252
|
+
});
|
|
11253
|
+
} catch (error) {
|
|
11254
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
|
|
11255
|
+
}
|
|
11048
11256
|
} else {
|
|
11049
11257
|
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", reason: readyListener.reason || "listener_unavailable", webhookId: listenerState.webhookId });
|
|
11050
11258
|
markClickUpWebhookInactive(worktree, listenerState, clickUpWebhookValidation.config);
|
|
@@ -11619,7 +11827,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
|
|
|
11619
11827
|
}
|
|
11620
11828
|
};
|
|
11621
11829
|
}
|
|
11622
|
-
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 };
|
|
11830
|
+
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 };
|
|
11623
11831
|
export {
|
|
11624
11832
|
OptimaPlugin as default
|
|
11625
11833
|
};
|
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
|
}
|
|
@@ -9385,6 +9405,31 @@ function recordClickUpCommentVersionProcessed({ ledgerPath, key, taskId, eventTy
|
|
|
9385
9405
|
function clickUpTaskIdFromPayload(payload = {}) {
|
|
9386
9406
|
return String(payload.task_id || payload.taskId || payload.task?.id || payload.task?.task_id || "").trim();
|
|
9387
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
|
+
}
|
|
9388
9433
|
function clickUpTaskName(task = {}) {
|
|
9389
9434
|
return String(task?.name || "").trim();
|
|
9390
9435
|
}
|
|
@@ -9578,6 +9623,45 @@ function formatClickUpWebhookPrompt({ eventType, taskId, payload, branch = "", w
|
|
|
9578
9623
|
deliveryEvidencePath ? `Final merge-trackable evidence must be written under ${deliveryEvidencePath}; use .optima only as local mirror/staging.` : null
|
|
9579
9624
|
].filter((part) => part !== null).join("\n");
|
|
9580
9625
|
}
|
|
9626
|
+
function clickUpNoAbandonmentRuleText() {
|
|
9627
|
+
return "No-abandonment rule: PM must keep working, or before stopping must post a ClickUp comment, hand off, remove PM as assignee, and assign the next owner.";
|
|
9628
|
+
}
|
|
9629
|
+
function buildClickUpStartupResumeComment({ taskId, result = {} } = {}) {
|
|
9630
|
+
const contextParts = [
|
|
9631
|
+
result.branch ? `- Branch: ${result.branch}` : null,
|
|
9632
|
+
result.worktree ? `- Worktree: ${result.worktree}` : null,
|
|
9633
|
+
result.deliveryEvidencePath ? `- Delivery evidence path: ${result.deliveryEvidencePath}` : null,
|
|
9634
|
+
result.evidencePath ? `- Local evidence path: ${result.evidencePath}` : null
|
|
9635
|
+
].filter(Boolean);
|
|
9636
|
+
return [
|
|
9637
|
+
`Startup reconciliation resumed ClickUp task ${taskId}.`,
|
|
9638
|
+
`Route result: ${result.action || "unknown"}${result.ok === false ? " (failed)" : ""}.`,
|
|
9639
|
+
result.sessionId ? `Session id: ${result.sessionId}.` : "Session id: unavailable.",
|
|
9640
|
+
contextParts.length ? contextParts.join("\n") : "Worktree/branch/delivery evidence path: unavailable.",
|
|
9641
|
+
clickUpNoAbandonmentRuleText()
|
|
9642
|
+
].join("\n");
|
|
9643
|
+
}
|
|
9644
|
+
function buildClickUpStartupBlockerComment({ taskId, error } = {}) {
|
|
9645
|
+
const summary = error?.message || String(error || "unknown error");
|
|
9646
|
+
return [
|
|
9647
|
+
`Startup reconciliation blocker for ClickUp task ${taskId}: ${summary}`,
|
|
9648
|
+
"Optima skipped this task for this startup pass and continued with the next PM-assigned task.",
|
|
9649
|
+
clickUpNoAbandonmentRuleText()
|
|
9650
|
+
].join("\n");
|
|
9651
|
+
}
|
|
9652
|
+
async function postClickUpStartupComment({ clickupClient, worktree, taskId, comment, type } = {}) {
|
|
9653
|
+
if (typeof clickupClient?.postTaskComment !== "function") {
|
|
9654
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_comment_unavailable", taskId, commentType: type });
|
|
9655
|
+
return { ok: false, skipped: true, reason: "post_task_comment_unavailable" };
|
|
9656
|
+
}
|
|
9657
|
+
try {
|
|
9658
|
+
await clickupClient.postTaskComment({ taskId, comment });
|
|
9659
|
+
return { ok: true };
|
|
9660
|
+
} catch (error) {
|
|
9661
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_comment_failed", taskId, commentType: type, message: error.message });
|
|
9662
|
+
return { ok: false, error: error.message };
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9581
9665
|
function appendClickUpWebhookLocalLog(worktree, entry) {
|
|
9582
9666
|
const logPath = clickUpWebhookLogPath(worktree);
|
|
9583
9667
|
fs2.mkdirSync(path2.dirname(logPath), { recursive: true });
|
|
@@ -9849,24 +9933,128 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
|
|
|
9849
9933
|
await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: nextMetadata });
|
|
9850
9934
|
const { [taskId]: _completedPending, ...remainingPending } = stateToPersist.pendingSessions || {};
|
|
9851
9935
|
stateToPersist = { ...stateToPersist, pendingSessions: remainingPending };
|
|
9852
|
-
return finish({ ok: true, action: "created_session", taskId, sessionId: pendingSessionId, eventKey });
|
|
9936
|
+
return finish({ ok: true, action: "created_session", taskId, sessionId: pendingSessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path });
|
|
9853
9937
|
}
|
|
9854
9938
|
const sessionId = String(existingSessionId);
|
|
9855
9939
|
if (await sessionExists(openCodeClient, sessionId)) {
|
|
9856
9940
|
if (typeof clickupClient?.updateTaskMetadata === "function") await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: metadataWithRouting });
|
|
9857
9941
|
await sendSessionEvent(openCodeClient, { sessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl });
|
|
9858
|
-
return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey });
|
|
9942
|
+
return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path });
|
|
9859
9943
|
}
|
|
9860
9944
|
const at = now().toISOString();
|
|
9861
9945
|
const incidentComment = `Optima webhook could not route this ClickUp event because OpenCode session ${sessionId} is missing on host ${host} at ${at}. No replacement session was created automatically.`;
|
|
9862
9946
|
appendClickUpWebhookLocalLog(worktree, { type: "missing_session", taskId, sessionId, host, at });
|
|
9863
9947
|
await clickupClient.postTaskComment({ taskId, comment: incidentComment });
|
|
9864
|
-
return finish({ ok: true, action: "missing_session_reported", taskId, sessionId, eventKey });
|
|
9948
|
+
return finish({ ok: true, action: "missing_session_reported", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path });
|
|
9865
9949
|
}
|
|
9866
9950
|
async function routeClickUpWebhookEvent(options = {}) {
|
|
9867
9951
|
const taskId = clickUpTaskIdFromPayload(options.payload || {});
|
|
9868
9952
|
return withClickUpTaskRouteLock(taskId, () => routeClickUpWebhookEventUnlocked(options));
|
|
9869
9953
|
}
|
|
9954
|
+
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 } = {}) {
|
|
9955
|
+
if (!config || !clickupClient?.listAssignedTasks) return { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
|
|
9956
|
+
let authorizedUserId = "";
|
|
9957
|
+
if (clickupClient?.getAuthorizedUser) {
|
|
9958
|
+
try {
|
|
9959
|
+
const userResponse = await clickupClient.getAuthorizedUser();
|
|
9960
|
+
authorizedUserId = String(userResponse?.user?.id || userResponse?.id || "").trim();
|
|
9961
|
+
} catch (error) {
|
|
9962
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_user_lookup_failed", message: error.message });
|
|
9963
|
+
}
|
|
9964
|
+
}
|
|
9965
|
+
let mutableState = state;
|
|
9966
|
+
const persistState = (nextState) => {
|
|
9967
|
+
mutableState = nextState;
|
|
9968
|
+
if (saveState) saveState(nextState);
|
|
9969
|
+
};
|
|
9970
|
+
const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
|
|
9971
|
+
const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
|
|
9972
|
+
const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0 };
|
|
9973
|
+
clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
|
|
9974
|
+
const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
|
|
9975
|
+
const tasks = clickUpTaskListItems(listed).slice(0, limit);
|
|
9976
|
+
for (const task of tasks) {
|
|
9977
|
+
const taskId = String(task?.id || task?.task_id || task?.taskId || "").trim();
|
|
9978
|
+
if (!taskId) {
|
|
9979
|
+
routed.ignored += 1;
|
|
9980
|
+
continue;
|
|
9981
|
+
}
|
|
9982
|
+
if (ignored.has(normalizeClickUpStatus(clickUpTaskStatus(task)))) {
|
|
9983
|
+
routed.ignored += 1;
|
|
9984
|
+
continue;
|
|
9985
|
+
}
|
|
9986
|
+
if (!isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) {
|
|
9987
|
+
routed.ignored += 1;
|
|
9988
|
+
continue;
|
|
9989
|
+
}
|
|
9990
|
+
try {
|
|
9991
|
+
const result = await routeClickUpWebhookEvent({
|
|
9992
|
+
payload: { webhook_id: mutableState.webhookId || "startup", event: "taskAssigneeUpdated", task_id: taskId, task, startup_reconciliation: true },
|
|
9993
|
+
config,
|
|
9994
|
+
state: mutableState,
|
|
9995
|
+
worktree,
|
|
9996
|
+
clickupClient,
|
|
9997
|
+
openCodeClient,
|
|
9998
|
+
sessionExists,
|
|
9999
|
+
createSession,
|
|
10000
|
+
sendSessionEvent,
|
|
10001
|
+
saveState: persistState,
|
|
10002
|
+
now
|
|
10003
|
+
});
|
|
10004
|
+
if (result?.ok && result.action !== "ignored") {
|
|
10005
|
+
routed.assigned += 1;
|
|
10006
|
+
await postClickUpStartupComment({
|
|
10007
|
+
clickupClient,
|
|
10008
|
+
worktree,
|
|
10009
|
+
taskId,
|
|
10010
|
+
type: "resume",
|
|
10011
|
+
comment: buildClickUpStartupResumeComment({ taskId, result })
|
|
10012
|
+
});
|
|
10013
|
+
}
|
|
10014
|
+
} catch (error) {
|
|
10015
|
+
routed.errors += 1;
|
|
10016
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_failed", taskId, message: error.message });
|
|
10017
|
+
await postClickUpStartupComment({
|
|
10018
|
+
clickupClient,
|
|
10019
|
+
worktree,
|
|
10020
|
+
taskId,
|
|
10021
|
+
type: "blocker",
|
|
10022
|
+
comment: buildClickUpStartupBlockerComment({ taskId, error })
|
|
10023
|
+
});
|
|
10024
|
+
}
|
|
10025
|
+
if (!lastWebhookMs || !clickupClient?.getTaskComments) continue;
|
|
10026
|
+
try {
|
|
10027
|
+
const commentsResponse = await clickupClient.getTaskComments({ taskId, start: lastWebhookMs, limit: CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT });
|
|
10028
|
+
for (const comment of clickUpCommentListItems(commentsResponse).slice(0, CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT)) {
|
|
10029
|
+
const commentMs = clickUpCommentUpdatedMs(comment);
|
|
10030
|
+
if (!commentMs || commentMs <= lastWebhookMs) continue;
|
|
10031
|
+
const authorId = clickUpCommentAuthorId(comment);
|
|
10032
|
+
if (authorId && (authorId === config.routing.ignoredCommentAuthorId || authorId === authorizedUserId)) continue;
|
|
10033
|
+
if (!clickUpCommentMentionsProductManager(comment, config.routing)) continue;
|
|
10034
|
+
const event = comment.date_updated || comment.dateUpdated || comment.updated_at || comment.updatedAt ? "taskCommentUpdated" : "taskCommentPosted";
|
|
10035
|
+
const result = await routeClickUpWebhookEvent({
|
|
10036
|
+
payload: { webhook_id: mutableState.webhookId || "startup", event, task_id: taskId, task, comment, history_item: { id: `startup-${taskId}-${comment.id || commentMs}` }, startup_reconciliation: true },
|
|
10037
|
+
config,
|
|
10038
|
+
state: mutableState,
|
|
10039
|
+
worktree,
|
|
10040
|
+
clickupClient,
|
|
10041
|
+
openCodeClient,
|
|
10042
|
+
sessionExists,
|
|
10043
|
+
createSession,
|
|
10044
|
+
sendSessionEvent,
|
|
10045
|
+
saveState: persistState,
|
|
10046
|
+
now
|
|
10047
|
+
});
|
|
10048
|
+
if (result?.ok && result.action !== "ignored") routed.comments += 1;
|
|
10049
|
+
}
|
|
10050
|
+
} catch (error) {
|
|
10051
|
+
routed.errors += 1;
|
|
10052
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_comments_failed", taskId, message: error.message });
|
|
10053
|
+
}
|
|
10054
|
+
}
|
|
10055
|
+
clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...routed });
|
|
10056
|
+
return { ok: routed.errors === 0, ...routed };
|
|
10057
|
+
}
|
|
9870
10058
|
function clickUpWebhookExpectedPath(config) {
|
|
9871
10059
|
try {
|
|
9872
10060
|
return new URL(config?.webhook?.publicUrl).pathname || "/";
|
|
@@ -9877,9 +10065,14 @@ function clickUpWebhookExpectedPath(config) {
|
|
|
9877
10065
|
async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date() } = {}) {
|
|
9878
10066
|
let payload = null;
|
|
9879
10067
|
let handled = null;
|
|
10068
|
+
let authenticatedWebhook = false;
|
|
10069
|
+
let receivedAt = null;
|
|
9880
10070
|
const finish = (result) => {
|
|
9881
10071
|
handled = result;
|
|
9882
|
-
|
|
10072
|
+
receivedAt ??= now();
|
|
10073
|
+
const auditState = authenticatedWebhook ? { ...state, lastWebhookAt: receivedAt.toISOString() } : state;
|
|
10074
|
+
if (saveState && authenticatedWebhook) saveState(auditState);
|
|
10075
|
+
writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state: auditState, handled, payload, at: receivedAt });
|
|
9883
10076
|
return result;
|
|
9884
10077
|
};
|
|
9885
10078
|
try {
|
|
@@ -9893,12 +10086,15 @@ async function handleClickUpWebhookRequest({ method = "POST", url = null, header
|
|
|
9893
10086
|
if (bodyBytes > maxBodyBytes) return finish({ ok: false, status: 413, reason: "body_too_large" });
|
|
9894
10087
|
const signature = headers["x-signature"] || headers["X-Signature"];
|
|
9895
10088
|
if (!verifyClickUpSignature(rawBody, signature, state?.secret)) return finish({ ok: false, status: 401, reason: "invalid_signature" });
|
|
10089
|
+
authenticatedWebhook = true;
|
|
10090
|
+
receivedAt = now();
|
|
9896
10091
|
try {
|
|
9897
10092
|
payload = JSON.parse(Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : String(rawBody));
|
|
9898
10093
|
} catch {
|
|
9899
10094
|
return finish({ ok: false, status: 400, reason: "invalid_json" });
|
|
9900
10095
|
}
|
|
9901
|
-
const
|
|
10096
|
+
const receivedState = { ...state, lastWebhookAt: receivedAt.toISOString() };
|
|
10097
|
+
const result = await routeClickUpWebhookEvent({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState });
|
|
9902
10098
|
return finish({ ok: result.ok, status: result.ok ? 200 : 422, result });
|
|
9903
10099
|
} catch (error) {
|
|
9904
10100
|
writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state, handled, error, payload, at: now() });
|
|
@@ -11052,6 +11248,18 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
11052
11248
|
listener: { bindHost: clickUpWebhookValidation.config.webhook.bindHost, bindPort: clickUpWebhookValidation.config.webhook.bindPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
11053
11249
|
}, clickUpWebhookValidation.config);
|
|
11054
11250
|
registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry });
|
|
11251
|
+
try {
|
|
11252
|
+
await reconcileClickUpStartup({
|
|
11253
|
+
config: clickUpWebhookValidation.config,
|
|
11254
|
+
state: activeState,
|
|
11255
|
+
worktree,
|
|
11256
|
+
clickupClient: lifecycleClickUpClient,
|
|
11257
|
+
openCodeClient: input.client,
|
|
11258
|
+
saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
|
|
11259
|
+
});
|
|
11260
|
+
} catch (error) {
|
|
11261
|
+
appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
|
|
11262
|
+
}
|
|
11055
11263
|
} else {
|
|
11056
11264
|
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", reason: readyListener.reason || "listener_unavailable", webhookId: listenerState.webhookId });
|
|
11057
11265
|
markClickUpWebhookInactive(worktree, listenerState, clickUpWebhookValidation.config);
|
|
@@ -11626,7 +11834,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
|
|
|
11626
11834
|
}
|
|
11627
11835
|
};
|
|
11628
11836
|
}
|
|
11629
|
-
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 };
|
|
11837
|
+
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 };
|
|
11630
11838
|
|
|
11631
11839
|
// src/sanitize_cli.js
|
|
11632
11840
|
var { migrateLegacyOptimaLayout: migrateLegacyOptimaLayout2 } = OptimaPlugin.__internals;
|