@defend-tech/opencode-optima 0.1.49 → 0.1.51

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 CHANGED
@@ -7938,6 +7938,7 @@ var CLICKUP_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
7938
7938
  var CLICKUP_WEBHOOK_REQUEST_TIMEOUT_MS = 1e4;
7939
7939
  var CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT = 50;
7940
7940
  var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
7941
+ var CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS = 3e4;
7941
7942
  var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
7942
7943
  var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
7943
7944
  var DISCUSSION_BACKFILL_FETCH_LIMIT = 100;
@@ -9013,6 +9014,11 @@ function normalizeOpenCodeBaseUrl(value, defaultValue = "http://127.0.0.1:3001")
9013
9014
  return candidate.replace(/\/+$/, "");
9014
9015
  }
9015
9016
  }
9017
+ function normalizeNonNegativeInteger(value, defaultValue) {
9018
+ if (value === void 0 || value === null || value === "") return defaultValue;
9019
+ const number = Number(value);
9020
+ return Number.isInteger(number) && number >= 0 ? number : defaultValue;
9021
+ }
9016
9022
  function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd()) {
9017
9023
  const raw = isPlainObject(rawClickUp) ? rawClickUp : {};
9018
9024
  const webhook = isPlainObject(raw.webhook) ? raw.webhook : {};
@@ -9028,7 +9034,11 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9028
9034
  apiToken: String(raw.api_token || raw.apiToken || "").trim(),
9029
9035
  log: normalizeClickUpWebhookLogLevel(raw.log),
9030
9036
  opencode: {
9031
- baseUrl: normalizeOpenCodeBaseUrl(opencode.base_url || opencode.baseUrl || raw.opencode_base_url || raw.opencodeBaseUrl)
9037
+ baseUrl: normalizeOpenCodeBaseUrl(opencode.base_url || opencode.baseUrl || raw.opencode_base_url || raw.opencodeBaseUrl),
9038
+ startupReconciliationDelayMs: normalizeNonNegativeInteger(
9039
+ opencode.startup_reconciliation_delay_ms ?? opencode.startupReconciliationDelayMs ?? raw.startup_reconciliation_delay_ms ?? raw.startupReconciliationDelayMs,
9040
+ CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS
9041
+ )
9032
9042
  },
9033
9043
  webhook: {
9034
9044
  publicUrl: String(webhook.public_url || webhook.publicUrl || "").trim(),
@@ -9709,7 +9719,13 @@ async function readOpenCodeJsonResponse(response, endpointName) {
9709
9719
  return { raw };
9710
9720
  }
9711
9721
  }
9712
- async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
9722
+ function appendDirectoryQuery(url, directory) {
9723
+ const value = String(directory || "").trim();
9724
+ if (!value) return url;
9725
+ const separator = url.includes("?") ? "&" : "?";
9726
+ return `${url}${separator}directory=${encodeURIComponent(value)}`;
9727
+ }
9728
+ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
9713
9729
  if (typeof fetchImpl !== "function") throw new Error("OpenCode direct prompt delivery requires fetch.");
9714
9730
  const root = normalizeOpenCodeBaseUrl(baseUrl, "");
9715
9731
  if (!root) throw new Error("OpenCode direct prompt delivery requires a base URL.");
@@ -9717,7 +9733,7 @@ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent,
9717
9733
  const attempts = [
9718
9734
  legacyOnly ? null : {
9719
9735
  name: "v2 prompt",
9720
- url: `${root}/api/session/${encodedSession}/prompt`,
9736
+ url: appendDirectoryQuery(`${root}/api/session/${encodedSession}/prompt`, directory),
9721
9737
  body: { prompt: { text }, delivery: "queue", resume: true },
9722
9738
  accept: (response, data) => {
9723
9739
  if (!response.ok) return null;
@@ -9727,7 +9743,7 @@ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent,
9727
9743
  },
9728
9744
  {
9729
9745
  name: "legacy async prompt",
9730
- url: `${root}/session/${encodedSession}/prompt_async`,
9746
+ url: appendDirectoryQuery(`${root}/session/${encodedSession}/prompt_async`, directory),
9731
9747
  body: { agent, parts: [{ type: "text", text }] },
9732
9748
  accept: (response, data) => {
9733
9749
  if (response.status !== 204 && !response.ok) return null;
@@ -9792,7 +9808,7 @@ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, direct
9792
9808
  firstError ??= error;
9793
9809
  }
9794
9810
  }
9795
- if (directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, fetchImpl, legacyOnly });
9811
+ if (directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly });
9796
9812
  if (firstError) throw firstError;
9797
9813
  throw new Error("OpenCode client does not expose session.prompt or session.promptAsync.");
9798
9814
  }
@@ -9827,6 +9843,11 @@ function openCodeMessageText(message) {
9827
9843
  for (const part of partList) parts.push(part?.text, part?.content);
9828
9844
  return parts.filter((value) => typeof value === "string").join("\n");
9829
9845
  }
9846
+ function openCodeMessageStableKey(message, index = 0) {
9847
+ const id = message?.id || message?.messageID || message?.messageId || message?.data?.id || null;
9848
+ if (id) return `id:${id}`;
9849
+ return `idx:${index}:text:${openCodeMessageText(message)}`;
9850
+ }
9830
9851
  async function verifyOpenCodeSessionEventDelivery(client, { sessionId, beforeMessages = null, expectedText = "", markers = [], attempts = 8, delayMs = 250 } = {}) {
9831
9852
  let lastError = "message_verification_unavailable";
9832
9853
  for (let attempt = 0; attempt < Math.max(1, attempts); attempt += 1) {
@@ -9834,13 +9855,15 @@ async function verifyOpenCodeSessionEventDelivery(client, { sessionId, beforeMes
9834
9855
  const afterMessages = await readOpenCodeSessionMessages(client, { sessionId, limit: 50 });
9835
9856
  if (!afterMessages) return { ok: false, reason: "message_verification_unavailable" };
9836
9857
  const beforeCount = Array.isArray(beforeMessages) ? beforeMessages.length : null;
9837
- const lastMessage = afterMessages.at(-1) || null;
9858
+ const beforeKeys = Array.isArray(beforeMessages) ? new Set(beforeMessages.map(openCodeMessageStableKey)) : null;
9859
+ const newMessages = beforeKeys ? afterMessages.filter((message, index) => !beforeKeys.has(openCodeMessageStableKey(message, index))) : afterMessages;
9860
+ const lastMessage = newMessages.at(-1) || afterMessages.at(-1) || null;
9838
9861
  const lastMessageId = lastMessage?.id || lastMessage?.messageID || lastMessage?.messageId || null;
9839
9862
  if (beforeCount !== null && afterMessages.length > beforeCount) return { ok: true, method: "message_count", beforeCount, afterCount: afterMessages.length, lastMessageId };
9840
9863
  const textNeedles = [expectedText, ...markers].map((value) => String(value || "").trim()).filter(Boolean);
9841
- const haystack = afterMessages.slice(-20).map(openCodeMessageText).join("\n");
9864
+ const haystack = newMessages.slice(-20).map(openCodeMessageText).join("\n");
9842
9865
  const matched = textNeedles.find((needle) => haystack.includes(needle));
9843
- if (matched) return { ok: true, method: "message_text", marker: matched, beforeCount, afterCount: afterMessages.length, lastMessageId };
9866
+ if (matched && (!beforeKeys || newMessages.length > 0)) return { ok: true, method: "message_text", marker: matched, beforeCount, afterCount: afterMessages.length, lastMessageId };
9844
9867
  lastError = "message_not_visible";
9845
9868
  } catch (error) {
9846
9869
  lastError = error.message || "message_verification_failed";
@@ -10259,11 +10282,44 @@ async function routeClickUpWebhookEvent(options = {}) {
10259
10282
  const taskId = clickUpTaskIdFromPayload(options.payload || {});
10260
10283
  return withClickUpTaskRouteLock(taskId, () => routeClickUpWebhookEventUnlocked(options));
10261
10284
  }
10285
+ function defaultStartupReconciliationScheduler(run, delayMs) {
10286
+ return setTimeout(run, delayMs);
10287
+ }
10288
+ function scheduleClickUpStartupReconciliation({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, scheduler = defaultStartupReconciliationScheduler, delayMs = config?.opencode?.startupReconciliationDelayMs ?? CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS, saveState = null, reconcile = reconcileClickUpStartup } = {}) {
10289
+ const normalizedDelayMs = normalizeNonNegativeInteger(delayMs, CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS);
10290
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_scheduled", delayMs: normalizedDelayMs, webhookId: state?.webhookId || null });
10291
+ const run = () => {
10292
+ Promise.resolve().then(() => reconcile({
10293
+ config,
10294
+ state,
10295
+ worktree,
10296
+ clickupClient,
10297
+ openCodeClient,
10298
+ saveState
10299
+ })).catch((error) => {
10300
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
10301
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
10302
+ });
10303
+ };
10304
+ const timer = scheduler(run, normalizedDelayMs);
10305
+ timer?.unref?.();
10306
+ return { scheduled: true, delayMs: normalizedDelayMs, timer };
10307
+ }
10262
10308
  async function reconcileClickUpStartup({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, waitForReadiness = waitForOpenCodeReadiness, saveState = null, now = () => /* @__PURE__ */ new Date(), limit = CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT } = {}) {
10263
- if (!config || !clickupClient?.listAssignedTasks) return { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
10309
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
10310
+ if (!config || !clickupClient?.listAssignedTasks) {
10311
+ const skipped = { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
10312
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...skipped });
10313
+ return skipped;
10314
+ }
10264
10315
  const readiness = await waitForReadiness(openCodeClient, { worktree, now });
10265
10316
  appendClickUpWebhookLocalLog(worktree, { type: readiness.ok ? "startup_reconciliation_readiness_ready" : "startup_reconciliation_readiness_failed", ...readiness });
10266
- if (!readiness.ok) return { ok: false, skipped: true, reason: "opencode_not_ready", readiness, assigned: 0, comments: 0, ignored: 0, errors: 1, undelivered: 1, tasks: [], validation: { undelivered: 1, emptyPromptSessions: [] } };
10317
+ if (!readiness.ok) {
10318
+ const failed = { ok: false, skipped: true, reason: "opencode_not_ready", readiness, assigned: 0, comments: 0, ignored: 0, errors: 1, undelivered: 1, tasks: [], validation: { undelivered: 1, emptyPromptSessions: [] } };
10319
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_failed", reason: failed.reason, readiness });
10320
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...failed });
10321
+ return failed;
10322
+ }
10267
10323
  let authorizedUserId = "";
10268
10324
  if (clickupClient?.getAuthorizedUser) {
10269
10325
  try {
@@ -10281,7 +10337,6 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10281
10337
  const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
10282
10338
  const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
10283
10339
  const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0, undelivered: 0, tasks: [] };
10284
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
10285
10340
  const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
10286
10341
  const tasks = clickUpTaskListItems(listed).slice(0, limit);
10287
10342
  for (const task of tasks) {
@@ -11599,18 +11654,16 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
11599
11654
  listener: { bindHost: clickUpWebhookValidation.config.webhook.bindHost, bindPort: clickUpWebhookValidation.config.webhook.bindPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
11600
11655
  }, clickUpWebhookValidation.config);
11601
11656
  registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry });
11602
- try {
11603
- await reconcileClickUpStartup({
11604
- config: clickUpWebhookValidation.config,
11605
- state: activeState,
11606
- worktree,
11607
- clickupClient: lifecycleClickUpClient,
11608
- openCodeClient: input.client,
11609
- saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
11610
- });
11611
- } catch (error) {
11612
- appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
11613
- }
11657
+ scheduleClickUpStartupReconciliation({
11658
+ config: clickUpWebhookValidation.config,
11659
+ state: activeState,
11660
+ worktree,
11661
+ clickupClient: lifecycleClickUpClient,
11662
+ openCodeClient: input.client,
11663
+ scheduler: input.startupReconciliationScheduler,
11664
+ delayMs: input.startupReconciliationDelayMs,
11665
+ saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
11666
+ });
11614
11667
  } else {
11615
11668
  clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", reason: readyListener.reason || "listener_unavailable", webhookId: listenerState.webhookId });
11616
11669
  markClickUpWebhookInactive(worktree, listenerState, clickUpWebhookValidation.config);
@@ -12185,7 +12238,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
12185
12238
  }
12186
12239
  };
12187
12240
  }
12188
- 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, createOpenCodeSession, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionMessages, reconcileClickUpStartup, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, verifyOpenCodeSessionEventDelivery, 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 };
12241
+ 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, createOpenCodeSession, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionMessages, reconcileClickUpStartup, scheduleClickUpStartupReconciliation, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, verifyOpenCodeSessionEventDelivery, 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 };
12189
12242
  export {
12190
12243
  OptimaPlugin as default
12191
12244
  };
@@ -7945,6 +7945,7 @@ var CLICKUP_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
7945
7945
  var CLICKUP_WEBHOOK_REQUEST_TIMEOUT_MS = 1e4;
7946
7946
  var CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT = 50;
7947
7947
  var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
7948
+ var CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS = 3e4;
7948
7949
  var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
7949
7950
  var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
7950
7951
  var DISCUSSION_BACKFILL_FETCH_LIMIT = 100;
@@ -9020,6 +9021,11 @@ function normalizeOpenCodeBaseUrl(value, defaultValue = "http://127.0.0.1:3001")
9020
9021
  return candidate.replace(/\/+$/, "");
9021
9022
  }
9022
9023
  }
9024
+ function normalizeNonNegativeInteger(value, defaultValue) {
9025
+ if (value === void 0 || value === null || value === "") return defaultValue;
9026
+ const number = Number(value);
9027
+ return Number.isInteger(number) && number >= 0 ? number : defaultValue;
9028
+ }
9023
9029
  function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd()) {
9024
9030
  const raw = isPlainObject(rawClickUp) ? rawClickUp : {};
9025
9031
  const webhook = isPlainObject(raw.webhook) ? raw.webhook : {};
@@ -9035,7 +9041,11 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9035
9041
  apiToken: String(raw.api_token || raw.apiToken || "").trim(),
9036
9042
  log: normalizeClickUpWebhookLogLevel(raw.log),
9037
9043
  opencode: {
9038
- baseUrl: normalizeOpenCodeBaseUrl(opencode.base_url || opencode.baseUrl || raw.opencode_base_url || raw.opencodeBaseUrl)
9044
+ baseUrl: normalizeOpenCodeBaseUrl(opencode.base_url || opencode.baseUrl || raw.opencode_base_url || raw.opencodeBaseUrl),
9045
+ startupReconciliationDelayMs: normalizeNonNegativeInteger(
9046
+ opencode.startup_reconciliation_delay_ms ?? opencode.startupReconciliationDelayMs ?? raw.startup_reconciliation_delay_ms ?? raw.startupReconciliationDelayMs,
9047
+ CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS
9048
+ )
9039
9049
  },
9040
9050
  webhook: {
9041
9051
  publicUrl: String(webhook.public_url || webhook.publicUrl || "").trim(),
@@ -9716,7 +9726,13 @@ async function readOpenCodeJsonResponse(response, endpointName) {
9716
9726
  return { raw };
9717
9727
  }
9718
9728
  }
9719
- async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
9729
+ function appendDirectoryQuery(url, directory) {
9730
+ const value = String(directory || "").trim();
9731
+ if (!value) return url;
9732
+ const separator = url.includes("?") ? "&" : "?";
9733
+ return `${url}${separator}directory=${encodeURIComponent(value)}`;
9734
+ }
9735
+ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
9720
9736
  if (typeof fetchImpl !== "function") throw new Error("OpenCode direct prompt delivery requires fetch.");
9721
9737
  const root = normalizeOpenCodeBaseUrl(baseUrl, "");
9722
9738
  if (!root) throw new Error("OpenCode direct prompt delivery requires a base URL.");
@@ -9724,7 +9740,7 @@ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent,
9724
9740
  const attempts = [
9725
9741
  legacyOnly ? null : {
9726
9742
  name: "v2 prompt",
9727
- url: `${root}/api/session/${encodedSession}/prompt`,
9743
+ url: appendDirectoryQuery(`${root}/api/session/${encodedSession}/prompt`, directory),
9728
9744
  body: { prompt: { text }, delivery: "queue", resume: true },
9729
9745
  accept: (response, data) => {
9730
9746
  if (!response.ok) return null;
@@ -9734,7 +9750,7 @@ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent,
9734
9750
  },
9735
9751
  {
9736
9752
  name: "legacy async prompt",
9737
- url: `${root}/session/${encodedSession}/prompt_async`,
9753
+ url: appendDirectoryQuery(`${root}/session/${encodedSession}/prompt_async`, directory),
9738
9754
  body: { agent, parts: [{ type: "text", text }] },
9739
9755
  accept: (response, data) => {
9740
9756
  if (response.status !== 204 && !response.ok) return null;
@@ -9799,7 +9815,7 @@ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, direct
9799
9815
  firstError ??= error;
9800
9816
  }
9801
9817
  }
9802
- if (directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, fetchImpl, legacyOnly });
9818
+ if (directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly });
9803
9819
  if (firstError) throw firstError;
9804
9820
  throw new Error("OpenCode client does not expose session.prompt or session.promptAsync.");
9805
9821
  }
@@ -9834,6 +9850,11 @@ function openCodeMessageText(message) {
9834
9850
  for (const part of partList) parts.push(part?.text, part?.content);
9835
9851
  return parts.filter((value) => typeof value === "string").join("\n");
9836
9852
  }
9853
+ function openCodeMessageStableKey(message, index = 0) {
9854
+ const id = message?.id || message?.messageID || message?.messageId || message?.data?.id || null;
9855
+ if (id) return `id:${id}`;
9856
+ return `idx:${index}:text:${openCodeMessageText(message)}`;
9857
+ }
9837
9858
  async function verifyOpenCodeSessionEventDelivery(client, { sessionId, beforeMessages = null, expectedText = "", markers = [], attempts = 8, delayMs = 250 } = {}) {
9838
9859
  let lastError = "message_verification_unavailable";
9839
9860
  for (let attempt = 0; attempt < Math.max(1, attempts); attempt += 1) {
@@ -9841,13 +9862,15 @@ async function verifyOpenCodeSessionEventDelivery(client, { sessionId, beforeMes
9841
9862
  const afterMessages = await readOpenCodeSessionMessages(client, { sessionId, limit: 50 });
9842
9863
  if (!afterMessages) return { ok: false, reason: "message_verification_unavailable" };
9843
9864
  const beforeCount = Array.isArray(beforeMessages) ? beforeMessages.length : null;
9844
- const lastMessage = afterMessages.at(-1) || null;
9865
+ const beforeKeys = Array.isArray(beforeMessages) ? new Set(beforeMessages.map(openCodeMessageStableKey)) : null;
9866
+ const newMessages = beforeKeys ? afterMessages.filter((message, index) => !beforeKeys.has(openCodeMessageStableKey(message, index))) : afterMessages;
9867
+ const lastMessage = newMessages.at(-1) || afterMessages.at(-1) || null;
9845
9868
  const lastMessageId = lastMessage?.id || lastMessage?.messageID || lastMessage?.messageId || null;
9846
9869
  if (beforeCount !== null && afterMessages.length > beforeCount) return { ok: true, method: "message_count", beforeCount, afterCount: afterMessages.length, lastMessageId };
9847
9870
  const textNeedles = [expectedText, ...markers].map((value) => String(value || "").trim()).filter(Boolean);
9848
- const haystack = afterMessages.slice(-20).map(openCodeMessageText).join("\n");
9871
+ const haystack = newMessages.slice(-20).map(openCodeMessageText).join("\n");
9849
9872
  const matched = textNeedles.find((needle) => haystack.includes(needle));
9850
- if (matched) return { ok: true, method: "message_text", marker: matched, beforeCount, afterCount: afterMessages.length, lastMessageId };
9873
+ if (matched && (!beforeKeys || newMessages.length > 0)) return { ok: true, method: "message_text", marker: matched, beforeCount, afterCount: afterMessages.length, lastMessageId };
9851
9874
  lastError = "message_not_visible";
9852
9875
  } catch (error) {
9853
9876
  lastError = error.message || "message_verification_failed";
@@ -10266,11 +10289,44 @@ async function routeClickUpWebhookEvent(options = {}) {
10266
10289
  const taskId = clickUpTaskIdFromPayload(options.payload || {});
10267
10290
  return withClickUpTaskRouteLock(taskId, () => routeClickUpWebhookEventUnlocked(options));
10268
10291
  }
10292
+ function defaultStartupReconciliationScheduler(run, delayMs) {
10293
+ return setTimeout(run, delayMs);
10294
+ }
10295
+ function scheduleClickUpStartupReconciliation({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, scheduler = defaultStartupReconciliationScheduler, delayMs = config?.opencode?.startupReconciliationDelayMs ?? CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS, saveState = null, reconcile = reconcileClickUpStartup } = {}) {
10296
+ const normalizedDelayMs = normalizeNonNegativeInteger(delayMs, CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS);
10297
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_scheduled", delayMs: normalizedDelayMs, webhookId: state?.webhookId || null });
10298
+ const run = () => {
10299
+ Promise.resolve().then(() => reconcile({
10300
+ config,
10301
+ state,
10302
+ worktree,
10303
+ clickupClient,
10304
+ openCodeClient,
10305
+ saveState
10306
+ })).catch((error) => {
10307
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
10308
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
10309
+ });
10310
+ };
10311
+ const timer = scheduler(run, normalizedDelayMs);
10312
+ timer?.unref?.();
10313
+ return { scheduled: true, delayMs: normalizedDelayMs, timer };
10314
+ }
10269
10315
  async function reconcileClickUpStartup({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, waitForReadiness = waitForOpenCodeReadiness, saveState = null, now = () => /* @__PURE__ */ new Date(), limit = CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT } = {}) {
10270
- if (!config || !clickupClient?.listAssignedTasks) return { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
10316
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
10317
+ if (!config || !clickupClient?.listAssignedTasks) {
10318
+ const skipped = { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
10319
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...skipped });
10320
+ return skipped;
10321
+ }
10271
10322
  const readiness = await waitForReadiness(openCodeClient, { worktree, now });
10272
10323
  appendClickUpWebhookLocalLog(worktree, { type: readiness.ok ? "startup_reconciliation_readiness_ready" : "startup_reconciliation_readiness_failed", ...readiness });
10273
- if (!readiness.ok) return { ok: false, skipped: true, reason: "opencode_not_ready", readiness, assigned: 0, comments: 0, ignored: 0, errors: 1, undelivered: 1, tasks: [], validation: { undelivered: 1, emptyPromptSessions: [] } };
10324
+ if (!readiness.ok) {
10325
+ const failed = { ok: false, skipped: true, reason: "opencode_not_ready", readiness, assigned: 0, comments: 0, ignored: 0, errors: 1, undelivered: 1, tasks: [], validation: { undelivered: 1, emptyPromptSessions: [] } };
10326
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_failed", reason: failed.reason, readiness });
10327
+ clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...failed });
10328
+ return failed;
10329
+ }
10274
10330
  let authorizedUserId = "";
10275
10331
  if (clickupClient?.getAuthorizedUser) {
10276
10332
  try {
@@ -10288,7 +10344,6 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10288
10344
  const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
10289
10345
  const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
10290
10346
  const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0, undelivered: 0, tasks: [] };
10291
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
10292
10347
  const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
10293
10348
  const tasks = clickUpTaskListItems(listed).slice(0, limit);
10294
10349
  for (const task of tasks) {
@@ -11606,18 +11661,16 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
11606
11661
  listener: { bindHost: clickUpWebhookValidation.config.webhook.bindHost, bindPort: clickUpWebhookValidation.config.webhook.bindPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
11607
11662
  }, clickUpWebhookValidation.config);
11608
11663
  registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry });
11609
- try {
11610
- await reconcileClickUpStartup({
11611
- config: clickUpWebhookValidation.config,
11612
- state: activeState,
11613
- worktree,
11614
- clickupClient: lifecycleClickUpClient,
11615
- openCodeClient: input.client,
11616
- saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
11617
- });
11618
- } catch (error) {
11619
- appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_failed", message: error.message });
11620
- }
11664
+ scheduleClickUpStartupReconciliation({
11665
+ config: clickUpWebhookValidation.config,
11666
+ state: activeState,
11667
+ worktree,
11668
+ clickupClient: lifecycleClickUpClient,
11669
+ openCodeClient: input.client,
11670
+ scheduler: input.startupReconciliationScheduler,
11671
+ delayMs: input.startupReconciliationDelayMs,
11672
+ saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
11673
+ });
11621
11674
  } else {
11622
11675
  clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", reason: readyListener.reason || "listener_unavailable", webhookId: listenerState.webhookId });
11623
11676
  markClickUpWebhookInactive(worktree, listenerState, clickUpWebhookValidation.config);
@@ -12192,7 +12245,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
12192
12245
  }
12193
12246
  };
12194
12247
  }
12195
- 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, createOpenCodeSession, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionMessages, reconcileClickUpStartup, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, verifyOpenCodeSessionEventDelivery, 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 };
12248
+ 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, createOpenCodeSession, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionMessages, reconcileClickUpStartup, scheduleClickUpStartupReconciliation, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, verifyOpenCodeSessionEventDelivery, 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 };
12196
12249
 
12197
12250
  // src/sanitize_cli.js
12198
12251
  var { migrateLegacyOptimaLayout: migrateLegacyOptimaLayout2 } = OptimaPlugin.__internals;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defend-tech/opencode-optima",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+ssh://git@github.com/defend-tech/opencode-optima.git"