@defend-tech/opencode-optima 0.1.66 → 0.1.68

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/Agents_Common.md CHANGED
@@ -13,13 +13,15 @@
13
13
  - Use the ClickUp skill and ClickUp MCP/tools for all ClickUp reads, writes, comments, field updates, status transitions, assignments, and dashboard operations; if unavailable or forbidden, state the sync blocker and leave a manual-sync payload in task/evidence.
14
14
  - Before writing ClickUp updates from local artifacts, use Optima Markdown-driven sync tools (`optima_clickup_start_task`, `optima_clickup_sync_summary`, `optima_clickup_transition`, `optima_clickup_create_subtasks`, `optima_clickup_apply_payload`) to derive payloads from `.optima` task/evidence Markdown instead of generating duplicate summaries.
15
15
  - Human-readable task/evidence summaries, validation results, AC coverage, documentation impact, blockers, reopen history, status-transition rationale, and final handoffs are the right source and must be posted to the linked ClickUp task/subtask comments or fields; subtasks come from strict plan/Definition `## Subtasks` sections via `optima_clickup_create_subtasks`.
16
+ - ClickUp comments are human-facing model updates only. Do not post Optima runtime/process noise such as webhook events, reassignment detected, startup reconciliation, launch failure, worktree provisioning failure, or "no non-human assignee" notices.
17
+ - ClickUp comments must use real Markdown line breaks. Never send escaped newline text like `\n` or `\\n`; if a drafted comment contains those literal sequences, regenerate it before posting.
16
18
  - WPM rewrites the ClickUp task description on initial pickup and again at plan completion with the complete current/final description of what must be done, distinct from comments.
17
19
  - Keep raw logs in evidence storage; do not paste raw logs wholesale into ClickUp. Post concise summaries, paths/links, or relevant excerpts only.
18
20
  - `product_manager` may investigate, answer, pre-estimate "a qué huele" small/medium/large plus rough story points, and operate ClickUp dashboards; development requests must be converted into properly routed ClickUp tasks.
19
21
  - ClickUp delivery types are `Tarea`, `Bug`, `Doc`, `PoC`; ignore `Idea`, legacy `Backlog` alias, `Hito`, `Nota de reunión`, and `Respuesta del formulario` unless converted or linked.
20
22
  - WPM estimates `Story Points` during `plan` and re-estimates on material plan changes.
21
23
  - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if a task worktree lacks that file, use Optima-provided fallback role context and configured ClickUp IDs instead of blocking solely on the missing repo-local file. Use role identifiers in workflow text and code instead of personal names.
22
- - ClickUp status actions: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, and test strategy, `in progress` execute, `validation` Tech Lead + Validator/QA gates, and parent post-approval merge automation. Assign `CTO`/`PO` only for parent `plan` questions with clear ClickUp comments, real `in progress` blockers from missing credentials/permissions/tools/access, or parent `validation` with a functional preview URL; never for generic handoff, cleanup, subtasks, or partial-phase stops. Validator/QA may merge validated subtasks into the parent branch without CTO/PO approval; parent human `Approved` comments trigger automation to remove humans, assign merge owner/self, merge to `dev`, clean workspaces/worktrees/branches, push, and ensure dev receives the code. `completed`/`Closed` ignore unless reopened.
24
+ - ClickUp status actions: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, and test strategy, `in progress` execute, `validation` Tech Lead + Validator/QA gates, and parent post-approval merge automation. Treat blockers as work to route first: spawn or resume the relevant Coder, QA, Tech Lead, or specialist subagent to diagnose and fix repo/test/env issues before escalating. Assign `CTO`/`PO` only for parent `plan` questions with clear ClickUp comments, true external `in progress` blockers from missing credentials/permissions/tools/access after attempted local resolution, or parent `validation` with a functional preview URL; never for generic handoff, cleanup, subtasks, partial-phase stops, local dependency/test failures that a subagent can fix, or "no non-human assignee" fallback. Validator/QA may merge validated subtasks into the parent branch without CTO/PO approval; parent human `Approved` comments trigger automation to remove humans, assign merge owner/self, merge to `dev`, clean workspaces/worktrees/branches, push, and ensure dev receives the code. `completed`/`Closed` ignore unless reopened.
23
25
  - One shared-worktree `implementation` task may be active; ClickUp-first delivery should use task-specific worktrees/branches.
24
26
  - Agent messages must start with `[Agent Message] From: <agent_name> To: <agent_name>`.
25
27
  - Clarifications, blockers, dependencies, and reviews go through PMA.
@@ -15,13 +15,15 @@
15
15
  - Use the ClickUp skill and ClickUp MCP/tools for all ClickUp reads, writes, comments, field updates, status transitions, assignments, and dashboard operations; if unavailable or forbidden, state the sync blocker and leave a manual-sync payload in task/evidence.
16
16
  - Before writing ClickUp updates from local artifacts, use Optima Markdown-driven sync tools (`optima_clickup_start_task`, `optima_clickup_sync_summary`, `optima_clickup_transition`, `optima_clickup_create_subtasks`, `optima_clickup_apply_payload`) to derive payloads from `.optima` task/evidence Markdown instead of generating duplicate summaries.
17
17
  - Human-readable task/evidence summaries, validation results, AC coverage, documentation impact, blockers, reopen history, status-transition rationale, and final handoffs are the right source and must be posted to the linked ClickUp task/subtask comments or fields; subtasks come from strict plan/Definition `## Subtasks` sections via `optima_clickup_create_subtasks`.
18
+ - ClickUp comments are human-facing model updates only. Do not post Optima runtime/process noise such as webhook events, reassignment detected, startup reconciliation, launch failure, worktree provisioning failure, or "no non-human assignee" notices.
19
+ - ClickUp comments must use real Markdown line breaks. Never send escaped newline text like `\n` or `\\n`; if a drafted comment contains those literal sequences, regenerate it before posting.
18
20
  - WPM rewrites the ClickUp task description on initial pickup and again at plan completion with the complete current/final description of what must be done, distinct from comments.
19
21
  - Keep raw logs in evidence storage; do not paste raw logs wholesale into ClickUp. Post concise summaries, paths/links, or relevant excerpts only.
20
22
  - `product_manager` may investigate, answer, pre-estimate "a qué huele" small/medium/large plus rough story points, and operate ClickUp dashboards; development requests must become routed ClickUp tasks.
21
23
  - ClickUp-first delivery types: `Tarea`, `Bug`, `Doc`, `PoC`; ignore `Idea`, legacy `Backlog` alias, `Hito`, `Nota de reunión`, `Respuesta del formulario` unless converted or linked.
22
24
  - WPM estimates `Story Points` during `plan` and re-estimates on material plan changes.
23
25
  - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if missing in a task worktree, use Optima fallback role context/configured ClickUp IDs instead of blocking solely on the missing file.
24
- - ClickUp-first statuses: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, and test strategy, `in progress` execute, `validation` Tech Lead + Validator/QA gates, and parent post-approval merge automation. Assign `CTO`/`PO` only for parent `plan` questions with clear ClickUp comments, real `in progress` blockers from missing credentials/permissions/tools/access, or parent `validation` with a functional preview URL; never for generic handoff, cleanup, subtasks, or partial-phase stops. Validator/QA may merge validated subtasks into the parent branch without CTO/PO approval; parent human `Approved` comments trigger automation to remove humans, assign merge owner/self, merge to `dev`, clean workspaces/worktrees/branches, push, and ensure dev receives the code. `completed`/`Closed` ignore unless reopened.
26
+ - ClickUp-first statuses: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, and test strategy, `in progress` execute, `validation` Tech Lead + Validator/QA gates, and parent post-approval merge automation. Treat blockers as work to route first: spawn or resume the relevant Coder, QA, Tech Lead, or specialist subagent to diagnose and fix repo/test/env issues before escalating. Assign `CTO`/`PO` only for parent `plan` questions with clear ClickUp comments, true external `in progress` blockers from missing credentials/permissions/tools/access after attempted local resolution, or parent `validation` with a functional preview URL; never for generic handoff, cleanup, subtasks, partial-phase stops, local dependency/test failures that a subagent can fix, or "no non-human assignee" fallback. Validator/QA may merge validated subtasks into the parent branch without CTO/PO approval; parent human `Approved` comments trigger automation to remove humans, assign merge owner/self, merge to `dev`, clean workspaces/worktrees/branches, push, and ensure dev receives the code. `completed`/`Closed` ignore unless reopened.
25
27
  - Signed agent-to-agent messages must start exactly: `[Agent Message] From: <agent_name> To: <agent_name>`.
26
28
  - Direct all clarifications, blockers, and specialist questions through PMA unless explicitly in a direct discussion-capable role.
27
29
  - Read relevant docs/tasks fully when they govern the current work. Prefer targeted CodeMap navigation before broad source search.
@@ -15,8 +15,9 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
15
15
  - `workflow_product_manager` owns delivery ops: ClickUp status, routing/handoffs, decomposition, validation gates, Git worktree/branch/PR flow, evidence, and closure.
16
16
  - `product_manager` remains compatibility/product/planning PMA for requirements, SCRs, product truth, rough pre-estimation, and default/legacy orchestration when ClickUp-first is not opted in. Do not remove, shadow, or break `product_manager`.
17
17
  - ClickUp Docs/tasks are source of truth for intent, state, comments, assignment, validation, and closure. Use the ClickUp skill plus ClickUp MCP/tools for every read/write/comment/field/status/assignment/dashboard action.
18
- - RULE NUMBER ONE: your operating objective is zero ClickUp tasks assigned to Workflow/Product Manager. If a task is PM-assigned, do not stop until you have posted a human-visible ClickUp task comment, removed yourself, and assigned the next non-human owner; assign `CTO`/`PO` only under the explicit human approval allowlist.
19
- - OpenCode session output is not visible to humans unless you post it to ClickUp. Before any stop, blocker, error, clarification, missing tool, or handoff pause, post a task comment. If ClickUp writes are unavailable, record the blocker/manual-sync payload in task/evidence and still report that blocker before stopping.
18
+ - RULE NUMBER ONE: your operating objective is delivered work, not merely zero PM-assigned tasks. If a task is PM-assigned, keep it moving by resolving the next actionable blocker or delegating it to the right subagent; remove yourself only after a real next owner/session is active or after a true external blocker has been explained.
19
+ - OpenCode session output is not visible to humans unless you post it to ClickUp. Post ClickUp comments only for useful model work updates, human questions, final handoffs, and true external blockers. Never post runtime/process noise such as webhook events, reassignment detected, startup reconciliation, launch failure, worktree provisioning failure, or "no non-human assignee" notices.
20
+ - ClickUp comments must be readable Markdown with real line breaks. Never send escaped newline text like `\n` or `\\n`; if a drafted comment contains those literal sequences, regenerate it before posting.
20
21
  - Keep raw logs in evidence; ClickUp gets summaries, paths/links, or excerpts only.
21
22
 
22
23
  ## Definition, Mirrors, Sessions
@@ -43,7 +44,7 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
43
44
  - Human approval allowlist: never assign `CTO`/`PO` except for parent `plan` questions with clear ClickUp comments, real `in progress` blockers from missing credentials/permissions/tools/access, or parent `validation` with a functional preview URL such as `https://<taskid>-preview.defend.tech`. Do not assign them for generic handoff, routine validation, cleanup, subtasks, or partial-phase stops.
44
45
  - `backlog`: ignore until prioritized.
45
46
  - `plan`: clarify AC/SCR/test strategy with Validator/QA; decompose; create/update Definition; estimate Story Points; remove PM assignee first; assign the next delivery owner. Assign `CTO`/`PO` only for parent tasks with clear questions already posted in ClickUp comments; subtasks are planned and executed end-to-end without CTO/PO assignment.
46
- - `in progress`: execute through the assigned delivery agent or workflow runner. Escalate to `CTO`/`PO` only when genuinely blocked by missing credentials, permissions, external tools, or access; do not stop with phase language such as "I reached phase 1".
47
+ - `in progress`: execute through the assigned delivery agent or workflow runner. Treat blockers as work to solve first: spawn or resume Coder for code/build/dependency failures, QA for validation/test/evidence failures, Tech Lead for architecture/review/merge failures, and the relevant specialist for domain blockers. Escalate to `CTO`/`PO` only when genuinely blocked by missing credentials, permissions, external tools, or access after local/subagent resolution attempts; do not stop with phase language such as "I reached phase 1" or "no non-human assignee is available".
47
48
  - `validation`: route Tech Lead for architecture/code/PR/standards/repo-skill review and Validator/QA for tests, Playwright/regression/coverage/evidence/final-doc checks. Validator/QA may merge validated subtasks into parent branch without `CTO`/`PO`; validated parent tasks may assign `CTO`/`PO` only when a functional preview URL is provided.
48
49
  - `merge`: parent-only post-approval automation after a human comments `Approved`; remove human assignees, assign yourself or the merge owner, merge parent PR into `dev`, clean workspaces/worktrees/branches, push to `dev`, and ensure the dev environment contains the code. Conflicts or merge failures return the affected item to `in progress`.
49
50
  - `completed` / `Closed`: no execution unless explicitly reopened.
@@ -66,7 +67,8 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
66
67
 
67
68
  - Orchestrate; do not silently implement specialist work yourself.
68
69
  - Delegate through ClickUp task/subtask assignment or task-specific specialist sessions with ClickUp context, AC, evidence, sync duties, branch target, Story Points, Definition link, final Documentation needs, and validation requirements.
69
- - Never abandon a PM-assigned task: keep working until unblocked and handed off, or post the stop/blocker/clarification/error as a ClickUp comment, remove Workflow/Product Manager, and assign the next delivery owner; assign `CTO`/`PO` only under the human approval allowlist.
70
+ - Never abandon a PM-assigned task: keep working until unblocked and handed off. If blocked, first create or resume the appropriate subagent and ask it to remove the blocker; only escalate true external blockers after documenting exactly what was tried and what external input is required.
71
+ - When a human comments or mentions `@Defend Tech Product Manager`, answer directly in the task with the requested clarification and then route the next action. Do not remove yourself merely because the human asks what is needed.
70
72
  - On pickup, rewrite the ClickUp task description with the complete current description of what must be done; do not rely on status comments.
71
73
  - At plan completion, rewrite the ClickUp task description again with the complete final plan/Definition, distinct from the plan comment.
72
74
  - Estimate Story Points during `plan`, write them to ClickUp `Story Points`, and re-estimate when material plan changes alter scope/risk.
package/dist/index.js CHANGED
@@ -8527,6 +8527,8 @@ var CLICKUP_WEBHOOK_REQUEST_TIMEOUT_MS = 1e4;
8527
8527
  var CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT = 50;
8528
8528
  var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
8529
8529
  var CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS = 3e4;
8530
+ var CLICKUP_WEBHOOK_STARTUP_COMMENT_LOOKBACK_MS = 6 * 60 * 60 * 1e3;
8531
+ var CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS = 5 * 60 * 1e3;
8530
8532
  var CLICKUP_WORKTREE_FAILURE_COMMENT_DEDUPE_MS = 10 * 60 * 1e3;
8531
8533
  var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
8532
8534
  var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
@@ -9807,6 +9809,10 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9807
9809
  startupReconciliationDelayMs: normalizeNonNegativeInteger(
9808
9810
  opencode.startup_reconciliation_delay_ms ?? opencode.startupReconciliationDelayMs ?? raw.startup_reconciliation_delay_ms ?? raw.startupReconciliationDelayMs,
9809
9811
  CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS
9812
+ ),
9813
+ assignmentWatchdogIntervalMs: normalizeNonNegativeInteger(
9814
+ opencode.assignment_watchdog_interval_ms ?? opencode.assignmentWatchdogIntervalMs ?? raw.assignment_watchdog_interval_ms ?? raw.assignmentWatchdogIntervalMs,
9815
+ CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS
9810
9816
  )
9811
9817
  },
9812
9818
  openchamber: {
@@ -10772,17 +10778,87 @@ function openCodeResultSummary(result) {
10772
10778
  status: result?.status || result?.response?.status || null
10773
10779
  };
10774
10780
  }
10781
+ function openCodeMessagePayload(message = {}) {
10782
+ if (!isPlainObject(message)) return {};
10783
+ const rawData = message.data;
10784
+ if (isPlainObject(rawData)) {
10785
+ return {
10786
+ ...rawData,
10787
+ id: rawData.id || message.id,
10788
+ time_created: rawData.time_created ?? message.time_created,
10789
+ time_updated: rawData.time_updated ?? message.time_updated
10790
+ };
10791
+ }
10792
+ if (typeof rawData === "string" && rawData.trim()) {
10793
+ try {
10794
+ const parsed = JSON.parse(rawData);
10795
+ if (isPlainObject(parsed)) {
10796
+ return {
10797
+ ...parsed,
10798
+ id: parsed.id || message.id,
10799
+ time_created: parsed.time_created ?? message.time_created,
10800
+ time_updated: parsed.time_updated ?? message.time_updated
10801
+ };
10802
+ }
10803
+ } catch {
10804
+ }
10805
+ }
10806
+ return message;
10807
+ }
10775
10808
  function normalizeOpenCodeMessageRole(message = {}) {
10776
- return message.role || message.info?.role || message.author?.role || message.type || "";
10809
+ const payload = openCodeMessagePayload(message);
10810
+ return payload.role || payload.info?.role || payload.author?.role || payload.type || "";
10777
10811
  }
10778
10812
  function normalizeOpenCodeMessageAgent(message = {}) {
10779
- return message.agent || message.info?.agent || message.mode?.agent || message.author?.agent || "";
10813
+ const payload = openCodeMessagePayload(message);
10814
+ return payload.agent || payload.info?.agent || payload.mode?.agent || payload.author?.agent || "";
10780
10815
  }
10781
10816
  function normalizeOpenCodeMessageMode(message = {}) {
10782
- return message.mode || message.info?.mode || message.metadata?.mode || "";
10817
+ const payload = openCodeMessagePayload(message);
10818
+ return payload.mode || payload.info?.mode || payload.metadata?.mode || "";
10783
10819
  }
10784
10820
  function normalizeOpenCodeMessageId(message = {}) {
10785
- return message.id || message.messageID || message.messageId || message.info?.id || message.data?.id || null;
10821
+ const payload = openCodeMessagePayload(message);
10822
+ return payload.id || payload.messageID || payload.messageId || payload.info?.id || payload.data?.id || message.id || null;
10823
+ }
10824
+ function openCodeMessageTimestampMs(message = {}, key = "updated") {
10825
+ const payload = openCodeMessagePayload(message);
10826
+ const time = payload.time || payload.info?.time || {};
10827
+ const value = key === "completed" ? payload.time_completed ?? payload.timeCompleted ?? time.completed : key === "created" ? payload.time_created ?? payload.timeCreated ?? time.created : payload.time_updated ?? payload.timeUpdated ?? time.updated ?? payload.time_created ?? payload.timeCreated ?? time.created;
10828
+ const number = Number(value);
10829
+ return Number.isFinite(number) && number > 0 ? number : 0;
10830
+ }
10831
+ function isOpenCodeAssistantMessageRunning(message = {}) {
10832
+ if (normalizeLooseToken(normalizeOpenCodeMessageRole(message)) !== "assistant") return false;
10833
+ const started = openCodeMessageTimestampMs(message, "created") > 0;
10834
+ if (!started) return false;
10835
+ return openCodeMessageTimestampMs(message, "completed") === 0;
10836
+ }
10837
+ async function inspectOpenCodeSessionActivity(client, { sessionId, directory, limit = 10 } = {}) {
10838
+ const messages = await readOpenCodeSessionMessages(client, { sessionId, directory, limit });
10839
+ if (!messages) return { ok: false, reason: "message_inspection_unavailable", sessionId, directory };
10840
+ const enriched = messages.map((message, index) => ({
10841
+ message,
10842
+ index,
10843
+ sortTime: openCodeMessageTimestampMs(message, "updated") || index,
10844
+ createdAt: openCodeMessageTimestampMs(message, "created"),
10845
+ completedAt: openCodeMessageTimestampMs(message, "completed")
10846
+ })).sort((a, b) => a.sortTime - b.sortTime || a.index - b.index);
10847
+ const assistantMessages = enriched.filter((entry) => normalizeLooseToken(normalizeOpenCodeMessageRole(entry.message)) === "assistant");
10848
+ const latestAssistant = assistantMessages.at(-1) || null;
10849
+ const latest = enriched.at(-1) || null;
10850
+ const running = latestAssistant ? isOpenCodeAssistantMessageRunning(latestAssistant.message) : false;
10851
+ return {
10852
+ ok: true,
10853
+ sessionId,
10854
+ directory,
10855
+ count: messages.length,
10856
+ running,
10857
+ latestMessageId: latest ? normalizeOpenCodeMessageId(latest.message) : null,
10858
+ latestAssistantMessageId: latestAssistant ? normalizeOpenCodeMessageId(latestAssistant.message) : null,
10859
+ latestAssistantCreatedAt: latestAssistant?.createdAt || 0,
10860
+ latestAssistantCompletedAt: latestAssistant?.completedAt || 0
10861
+ };
10786
10862
  }
10787
10863
  function summarizeOpenCodeMessages(messages = [], { snippetLength = 160, maxMessages = 50 } = {}) {
10788
10864
  const list = Array.isArray(messages) ? messages.slice(0, maxMessages) : [];
@@ -10922,8 +10998,9 @@ async function probeOpenCodeSessionControl(client, { directory, agent, omitAgent
10922
10998
  };
10923
10999
  }
10924
11000
  function openCodeMessageText(message) {
10925
- const parts = [message?.text, message?.content, message?.message, message?.body?.text, message?.data?.text];
10926
- const partList = Array.isArray(message?.parts) ? message.parts : Array.isArray(message?.body?.parts) ? message.body.parts : [];
11001
+ const payload = openCodeMessagePayload(message);
11002
+ const parts = [payload?.text, payload?.content, payload?.message, payload?.body?.text, payload?.data?.text];
11003
+ const partList = Array.isArray(payload?.parts) ? payload.parts : Array.isArray(payload?.body?.parts) ? payload.body.parts : [];
10927
11004
  for (const part of partList) parts.push(part?.text, part?.content);
10928
11005
  return parts.filter((value) => typeof value === "string").join("\n");
10929
11006
  }
@@ -11141,6 +11218,7 @@ async function cleanupManagedClickUpWebhook(entry = {}, { timeoutMs = CLICKUP_WE
11141
11218
  entry.cleaning = true;
11142
11219
  const cleanup = (async () => {
11143
11220
  clickUpWebhookLifecycleLog(entry.worktree, { type: "cleanup_started", reason, key: entry.key, webhookId: entry.state?.webhookId });
11221
+ if (entry.assignmentWatchdog?.interval) clearInterval(entry.assignmentWatchdog.interval);
11144
11222
  const closeResult = await closeClickUpWebhookServer(entry.listener?.server);
11145
11223
  if (entry.listenerRegistry && entry.listener?.key) entry.listenerRegistry.delete(entry.listener.key);
11146
11224
  const preservedResult = { ok: true, skipped: true, reason: "remote_webhook_preserved" };
@@ -11154,10 +11232,10 @@ async function cleanupManagedClickUpWebhook(entry = {}, { timeoutMs = CLICKUP_WE
11154
11232
  if (result?.timeout) clickUpWebhookLifecycleLog(entry.worktree, { type: "cleanup_timeout", reason, webhookId: entry.state?.webhookId });
11155
11233
  return result;
11156
11234
  }
11157
- function registerClickUpWebhookLifecycle({ config, state, worktree, clickupClient, listener, listenerRegistry = activeClickUpWebhookListeners } = {}) {
11235
+ function registerClickUpWebhookLifecycle({ config, state, worktree, clickupClient, listener, listenerRegistry = activeClickUpWebhookListeners, assignmentWatchdog = null } = {}) {
11158
11236
  if (!isClickUpWebhookStateActive(state, config)) return null;
11159
11237
  const key = managedClickUpWebhookKey({ worktree, state, config });
11160
- const entry = { key, config, state, worktree, clickupClient, listener, listenerRegistry, registeredAt: (/* @__PURE__ */ new Date()).toISOString() };
11238
+ const entry = { key, config, state, worktree, clickupClient, listener, listenerRegistry, assignmentWatchdog, registeredAt: (/* @__PURE__ */ new Date()).toISOString() };
11161
11239
  activeClickUpWebhookLifecycleRegistry.set(key, entry);
11162
11240
  installClickUpWebhookSignalHandlers();
11163
11241
  clickUpWebhookLifecycleLog(worktree, { type: "lifecycle_registered", key, webhookId: state.webhookId });
@@ -11447,19 +11525,54 @@ function scheduleClickUpStartupReconciliation({ config, state = {}, worktree = p
11447
11525
  timer?.unref?.();
11448
11526
  return { scheduled: true, delayMs: normalizedDelayMs, timer };
11449
11527
  }
11450
- 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 } = {}) {
11451
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
11528
+ function scheduleClickUpAssignmentWatchdog({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, scheduler = setInterval, intervalMs = config?.opencode?.assignmentWatchdogIntervalMs ?? CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS, saveState = null, reconcile = reconcileClickUpStartup } = {}) {
11529
+ const normalizedIntervalMs = normalizeNonNegativeInteger(intervalMs, CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS);
11530
+ if (!normalizedIntervalMs) {
11531
+ clickUpWebhookLifecycleLog(worktree, { type: "assignment_watchdog_disabled", webhookId: state?.webhookId || null });
11532
+ return { scheduled: false, intervalMs: 0, reason: "disabled" };
11533
+ }
11534
+ let running = false;
11535
+ const run = () => {
11536
+ if (running) {
11537
+ appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_tick_skipped", reason: "previous_tick_running" });
11538
+ return;
11539
+ }
11540
+ running = true;
11541
+ const latestState = readClickUpWebhookState(worktree, config);
11542
+ Promise.resolve().then(() => reconcile({
11543
+ config,
11544
+ state: latestState?.webhookId ? latestState : state,
11545
+ worktree,
11546
+ clickupClient,
11547
+ openCodeClient,
11548
+ saveState,
11549
+ assignmentMode: "watchdog"
11550
+ })).catch((error) => {
11551
+ clickUpWebhookLifecycleLog(worktree, { type: "assignment_watchdog_failed", message: error.message });
11552
+ appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_failed", message: error.message });
11553
+ }).finally(() => {
11554
+ running = false;
11555
+ });
11556
+ };
11557
+ const interval = scheduler(run, normalizedIntervalMs);
11558
+ interval?.unref?.();
11559
+ clickUpWebhookLifecycleLog(worktree, { type: "assignment_watchdog_scheduled", intervalMs: normalizedIntervalMs, webhookId: state?.webhookId || null });
11560
+ return { scheduled: true, intervalMs: normalizedIntervalMs, interval };
11561
+ }
11562
+ 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, assignmentMode = "startup" } = {}) {
11563
+ const watchdogMode = assignmentMode === "watchdog";
11564
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_started" : "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
11452
11565
  if (!config || !clickupClient?.listAssignedTasks) {
11453
11566
  const skipped = { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
11454
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...skipped });
11567
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_finished" : "startup_reconciliation_finished", ...skipped });
11455
11568
  return skipped;
11456
11569
  }
11457
11570
  const readiness = await waitForReadiness(openCodeClient, { worktree, now, opencodeBaseUrl: config.opencode?.baseUrl });
11458
11571
  appendClickUpWebhookLocalLog(worktree, { type: readiness.ok ? "startup_reconciliation_readiness_ready" : "startup_reconciliation_readiness_failed", ...readiness });
11459
11572
  if (!readiness.ok) {
11460
11573
  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: [] } };
11461
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_failed", reason: failed.reason, readiness });
11462
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...failed });
11574
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_failed" : "startup_reconciliation_failed", reason: failed.reason, readiness });
11575
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_finished" : "startup_reconciliation_finished", ...failed });
11463
11576
  return failed;
11464
11577
  }
11465
11578
  let authorizedUserId = "";
@@ -11477,6 +11590,7 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11477
11590
  if (saveState) saveState(nextState);
11478
11591
  };
11479
11592
  const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
11593
+ const startupCommentStartMs = lastWebhookMs || Math.max(0, now().getTime() - CLICKUP_WEBHOOK_STARTUP_COMMENT_LOOKBACK_MS);
11480
11594
  const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
11481
11595
  const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0, undelivered: 0, tasks: [] };
11482
11596
  const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
@@ -11495,9 +11609,29 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11495
11609
  routed.ignored += 1;
11496
11610
  continue;
11497
11611
  }
11612
+ if (watchdogMode) {
11613
+ const existingMetadata = clickUpTaskAgentMetadata(task, config.routing.metadataFieldId);
11614
+ const metadata = normalizeAgentMetadataJson(existingMetadata);
11615
+ const rawSessionId = getNestedMetadataValue(metadata, config.routing.metadataKey);
11616
+ const sessionId = typeof rawSessionId === "string" && rawSessionId.startsWith("ses_") ? rawSessionId : "";
11617
+ const directory = String(getNestedMetadataValue(metadata, "task.worktree") || "").trim();
11618
+ if (sessionId) {
11619
+ try {
11620
+ const activity = await inspectOpenCodeSessionActivity(openCodeClient, { sessionId, directory });
11621
+ appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_session_activity", taskId, sessionId, directory: directory || null, ...activity });
11622
+ if (activity.running) {
11623
+ routed.ignored += 1;
11624
+ routed.tasks.push({ taskId, action: "ignored", ok: true, sessionId, branch: getNestedMetadataValue(metadata, "task.branch") || null, worktree: directory || null, verification: "session_running", delivered: false });
11625
+ continue;
11626
+ }
11627
+ } catch (error) {
11628
+ appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_session_activity_failed", taskId, sessionId, directory: directory || null, message: error.message });
11629
+ }
11630
+ }
11631
+ }
11498
11632
  try {
11499
11633
  const result = await routeClickUpWebhookEvent({
11500
- payload: { webhook_id: mutableState.webhookId || "startup", event: "taskAssigneeUpdated", task_id: taskId, task, startup_reconciliation: true },
11634
+ payload: { webhook_id: mutableState.webhookId || "startup", event: "taskAssigneeUpdated", task_id: taskId, task, startup_reconciliation: true, assignment_watchdog: watchdogMode },
11501
11635
  config,
11502
11636
  state: mutableState,
11503
11637
  worktree,
@@ -11547,12 +11681,12 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11547
11681
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: "error", sessionId: null, reason: "startup_reconciliation_task_failed" });
11548
11682
  await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "startup_reconciliation_task_failed", source: "startup_reconciliation_task" });
11549
11683
  }
11550
- if (!lastWebhookMs || !clickupClient?.getTaskComments) continue;
11684
+ if (!startupCommentStartMs || !clickupClient?.getTaskComments) continue;
11551
11685
  try {
11552
- const commentsResponse = await clickupClient.getTaskComments({ taskId, start: lastWebhookMs, limit: CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT });
11686
+ const commentsResponse = await clickupClient.getTaskComments({ taskId, start: startupCommentStartMs, limit: CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT });
11553
11687
  for (const comment of clickUpCommentListItems(commentsResponse).slice(0, CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT)) {
11554
11688
  const commentMs = clickUpCommentUpdatedMs(comment);
11555
- if (!commentMs || commentMs <= lastWebhookMs) continue;
11689
+ if (!commentMs || commentMs <= startupCommentStartMs) continue;
11556
11690
  const authorId = clickUpCommentAuthorId(comment);
11557
11691
  if (authorId && (authorId === config.routing.ignoredCommentAuthorId || authorId === authorizedUserId)) continue;
11558
11692
  if (!clickUpCommentMentionsProductManager(comment, config.routing)) continue;
@@ -11579,11 +11713,11 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11579
11713
  await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "startup_reconciliation_comments_failed", source: "startup_reconciliation_comments" });
11580
11714
  }
11581
11715
  }
11582
- const emptyPromptSessions = routed.tasks.filter((task) => task.delivered === false && task.sessionId);
11716
+ const emptyPromptSessions = routed.tasks.filter((task) => task.delivered === false && task.sessionId && task.verification !== "session_running");
11583
11717
  if (emptyPromptSessions.length > 0) {
11584
11718
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_validation_failed", tasks: emptyPromptSessions });
11585
11719
  }
11586
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...routed });
11720
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_finished" : "startup_reconciliation_finished", ...routed });
11587
11721
  return { ok: routed.errors === 0 && routed.undelivered === 0, ...routed, validation: { undelivered: routed.undelivered, emptyPromptSessions } };
11588
11722
  }
11589
11723
  function clickUpWebhookExpectedPath(config) {
@@ -11593,17 +11727,49 @@ function clickUpWebhookExpectedPath(config) {
11593
11727
  return "/";
11594
11728
  }
11595
11729
  }
11596
- async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date(), sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery } = {}) {
11730
+ function summarizeClickUpRouteForLog(result = {}, payload = {}) {
11731
+ return {
11732
+ taskId: result.taskId || clickUpTaskIdFromPayload(payload || {}) || null,
11733
+ event: clickUpEventType(payload || {}) || null,
11734
+ eventKey: result.eventKey || clickUpWebhookEventKey(payload || {}) || null,
11735
+ ok: result.ok === true,
11736
+ action: result.action || null,
11737
+ reason: result.reason || null,
11738
+ sessionId: result.sessionId || result.replacementSessionId || null,
11739
+ branch: result.branch || null,
11740
+ worktree: result.worktree || null
11741
+ };
11742
+ }
11743
+ function runClickUpWebhookRouteInBackground({ payload, config, state, worktree, clickupClient, openCodeClient, saveState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, now } = {}) {
11744
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11745
+ appendClickUpWebhookLocalLog(worktree, { type: "webhook_route_started", taskId: clickUpTaskIdFromPayload(payload || {}) || null, event: clickUpEventType(payload || {}) || null, eventKey: clickUpWebhookEventKey(payload || {}) || null, async: true, startedAt });
11746
+ const route = Promise.resolve().then(() => routeClickUpWebhookEvent({ payload, config, state, worktree, clickupClient, openCodeClient, saveState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, now })).then((result) => {
11747
+ appendClickUpWebhookLocalLog(worktree, { type: "webhook_route_finished", async: true, ...summarizeClickUpRouteForLog(result, payload) });
11748
+ return result;
11749
+ }).catch((error) => {
11750
+ appendClickUpWebhookLocalLog(worktree, { type: "webhook_route_failed", taskId: clickUpTaskIdFromPayload(payload || {}) || null, event: clickUpEventType(payload || {}) || null, eventKey: clickUpWebhookEventKey(payload || {}) || null, async: true, message: error.message });
11751
+ return { ok: false, action: "error", reason: "background_route_failed", message: error.message };
11752
+ });
11753
+ route.catch(() => {
11754
+ });
11755
+ return route;
11756
+ }
11757
+ async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date(), sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, asyncRouting = false } = {}) {
11597
11758
  let payload = null;
11598
11759
  let handled = null;
11599
11760
  let authenticatedWebhook = false;
11600
11761
  let receivedAt = null;
11601
11762
  let activeState = state;
11763
+ let latestPersistedState = state;
11764
+ const persistState = (nextState) => {
11765
+ latestPersistedState = nextState;
11766
+ if (saveState) saveState(nextState);
11767
+ };
11602
11768
  const finish = (result) => {
11603
11769
  handled = result;
11604
11770
  receivedAt ??= now();
11605
- const auditState = authenticatedWebhook ? { ...activeState, lastWebhookAt: receivedAt.toISOString() } : activeState;
11606
- if (saveState && authenticatedWebhook) saveState(auditState);
11771
+ const auditState = authenticatedWebhook ? { ...latestPersistedState, lastWebhookAt: receivedAt.toISOString() } : latestPersistedState;
11772
+ if (saveState && authenticatedWebhook) persistState(auditState);
11607
11773
  writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state: auditState, handled, payload, at: receivedAt });
11608
11774
  return result;
11609
11775
  };
@@ -11630,7 +11796,12 @@ async function handleClickUpWebhookRequest({ method = "POST", url = null, header
11630
11796
  return finish({ ok: false, status: 400, reason: "invalid_json" });
11631
11797
  }
11632
11798
  const receivedState = { ...activeState, lastWebhookAt: receivedAt.toISOString() };
11633
- const result = await routeClickUpWebhookEvent({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery });
11799
+ persistState(receivedState);
11800
+ if (asyncRouting) {
11801
+ runClickUpWebhookRouteInBackground({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState: persistState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, now });
11802
+ return finish({ ok: true, status: 200, reason: "accepted", result: { action: "accepted", taskId: clickUpTaskIdFromPayload(payload), eventKey: clickUpWebhookEventKey(payload) } });
11803
+ }
11804
+ const result = await routeClickUpWebhookEvent({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState: persistState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery });
11634
11805
  return finish({ ok: result.ok, status: result.ok ? 200 : 422, result });
11635
11806
  } catch (error) {
11636
11807
  writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state, handled, error, payload, at: now() });
@@ -11707,7 +11878,8 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
11707
11878
  worktree,
11708
11879
  clickupClient,
11709
11880
  openCodeClient,
11710
- saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, config)
11881
+ saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, config),
11882
+ asyncRouting: true
11711
11883
  });
11712
11884
  res.writeHead(handled.status, { "Content-Type": "application/json" });
11713
11885
  res.end(JSON.stringify({ ok: handled.ok, reason: handled.reason, result: handled.result?.action }));
@@ -12859,7 +13031,17 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
12859
13031
  ...listenerState,
12860
13032
  listener: { bindHost: clickUpWebhookValidation.config.webhook.bindHost, bindPort: clickUpWebhookValidation.config.webhook.bindPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
12861
13033
  }, clickUpWebhookValidation.config);
12862
- registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry });
13034
+ const assignmentWatchdog = scheduleClickUpAssignmentWatchdog({
13035
+ config: clickUpWebhookValidation.config,
13036
+ state: activeState,
13037
+ worktree,
13038
+ clickupClient: lifecycleClickUpClient,
13039
+ openCodeClient: input.client,
13040
+ scheduler: input.assignmentWatchdogScheduler,
13041
+ intervalMs: input.assignmentWatchdogIntervalMs,
13042
+ saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
13043
+ });
13044
+ registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry, assignmentWatchdog });
12863
13045
  scheduleClickUpStartupReconciliation({
12864
13046
  config: clickUpWebhookValidation.config,
12865
13047
  state: activeState,
@@ -13515,7 +13697,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
13515
13697
  }
13516
13698
  };
13517
13699
  }
13518
- 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, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
13700
+ 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, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
13519
13701
  export {
13520
13702
  OptimaPlugin as default
13521
13703
  };
@@ -8534,6 +8534,8 @@ var CLICKUP_WEBHOOK_REQUEST_TIMEOUT_MS = 1e4;
8534
8534
  var CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT = 50;
8535
8535
  var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
8536
8536
  var CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS = 3e4;
8537
+ var CLICKUP_WEBHOOK_STARTUP_COMMENT_LOOKBACK_MS = 6 * 60 * 60 * 1e3;
8538
+ var CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS = 5 * 60 * 1e3;
8537
8539
  var CLICKUP_WORKTREE_FAILURE_COMMENT_DEDUPE_MS = 10 * 60 * 1e3;
8538
8540
  var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
8539
8541
  var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
@@ -9814,6 +9816,10 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9814
9816
  startupReconciliationDelayMs: normalizeNonNegativeInteger(
9815
9817
  opencode.startup_reconciliation_delay_ms ?? opencode.startupReconciliationDelayMs ?? raw.startup_reconciliation_delay_ms ?? raw.startupReconciliationDelayMs,
9816
9818
  CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS
9819
+ ),
9820
+ assignmentWatchdogIntervalMs: normalizeNonNegativeInteger(
9821
+ opencode.assignment_watchdog_interval_ms ?? opencode.assignmentWatchdogIntervalMs ?? raw.assignment_watchdog_interval_ms ?? raw.assignmentWatchdogIntervalMs,
9822
+ CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS
9817
9823
  )
9818
9824
  },
9819
9825
  openchamber: {
@@ -10779,17 +10785,87 @@ function openCodeResultSummary(result) {
10779
10785
  status: result?.status || result?.response?.status || null
10780
10786
  };
10781
10787
  }
10788
+ function openCodeMessagePayload(message = {}) {
10789
+ if (!isPlainObject(message)) return {};
10790
+ const rawData = message.data;
10791
+ if (isPlainObject(rawData)) {
10792
+ return {
10793
+ ...rawData,
10794
+ id: rawData.id || message.id,
10795
+ time_created: rawData.time_created ?? message.time_created,
10796
+ time_updated: rawData.time_updated ?? message.time_updated
10797
+ };
10798
+ }
10799
+ if (typeof rawData === "string" && rawData.trim()) {
10800
+ try {
10801
+ const parsed = JSON.parse(rawData);
10802
+ if (isPlainObject(parsed)) {
10803
+ return {
10804
+ ...parsed,
10805
+ id: parsed.id || message.id,
10806
+ time_created: parsed.time_created ?? message.time_created,
10807
+ time_updated: parsed.time_updated ?? message.time_updated
10808
+ };
10809
+ }
10810
+ } catch {
10811
+ }
10812
+ }
10813
+ return message;
10814
+ }
10782
10815
  function normalizeOpenCodeMessageRole(message = {}) {
10783
- return message.role || message.info?.role || message.author?.role || message.type || "";
10816
+ const payload = openCodeMessagePayload(message);
10817
+ return payload.role || payload.info?.role || payload.author?.role || payload.type || "";
10784
10818
  }
10785
10819
  function normalizeOpenCodeMessageAgent(message = {}) {
10786
- return message.agent || message.info?.agent || message.mode?.agent || message.author?.agent || "";
10820
+ const payload = openCodeMessagePayload(message);
10821
+ return payload.agent || payload.info?.agent || payload.mode?.agent || payload.author?.agent || "";
10787
10822
  }
10788
10823
  function normalizeOpenCodeMessageMode(message = {}) {
10789
- return message.mode || message.info?.mode || message.metadata?.mode || "";
10824
+ const payload = openCodeMessagePayload(message);
10825
+ return payload.mode || payload.info?.mode || payload.metadata?.mode || "";
10790
10826
  }
10791
10827
  function normalizeOpenCodeMessageId(message = {}) {
10792
- return message.id || message.messageID || message.messageId || message.info?.id || message.data?.id || null;
10828
+ const payload = openCodeMessagePayload(message);
10829
+ return payload.id || payload.messageID || payload.messageId || payload.info?.id || payload.data?.id || message.id || null;
10830
+ }
10831
+ function openCodeMessageTimestampMs(message = {}, key = "updated") {
10832
+ const payload = openCodeMessagePayload(message);
10833
+ const time = payload.time || payload.info?.time || {};
10834
+ const value = key === "completed" ? payload.time_completed ?? payload.timeCompleted ?? time.completed : key === "created" ? payload.time_created ?? payload.timeCreated ?? time.created : payload.time_updated ?? payload.timeUpdated ?? time.updated ?? payload.time_created ?? payload.timeCreated ?? time.created;
10835
+ const number = Number(value);
10836
+ return Number.isFinite(number) && number > 0 ? number : 0;
10837
+ }
10838
+ function isOpenCodeAssistantMessageRunning(message = {}) {
10839
+ if (normalizeLooseToken(normalizeOpenCodeMessageRole(message)) !== "assistant") return false;
10840
+ const started = openCodeMessageTimestampMs(message, "created") > 0;
10841
+ if (!started) return false;
10842
+ return openCodeMessageTimestampMs(message, "completed") === 0;
10843
+ }
10844
+ async function inspectOpenCodeSessionActivity(client, { sessionId, directory, limit = 10 } = {}) {
10845
+ const messages = await readOpenCodeSessionMessages(client, { sessionId, directory, limit });
10846
+ if (!messages) return { ok: false, reason: "message_inspection_unavailable", sessionId, directory };
10847
+ const enriched = messages.map((message, index) => ({
10848
+ message,
10849
+ index,
10850
+ sortTime: openCodeMessageTimestampMs(message, "updated") || index,
10851
+ createdAt: openCodeMessageTimestampMs(message, "created"),
10852
+ completedAt: openCodeMessageTimestampMs(message, "completed")
10853
+ })).sort((a, b) => a.sortTime - b.sortTime || a.index - b.index);
10854
+ const assistantMessages = enriched.filter((entry) => normalizeLooseToken(normalizeOpenCodeMessageRole(entry.message)) === "assistant");
10855
+ const latestAssistant = assistantMessages.at(-1) || null;
10856
+ const latest = enriched.at(-1) || null;
10857
+ const running = latestAssistant ? isOpenCodeAssistantMessageRunning(latestAssistant.message) : false;
10858
+ return {
10859
+ ok: true,
10860
+ sessionId,
10861
+ directory,
10862
+ count: messages.length,
10863
+ running,
10864
+ latestMessageId: latest ? normalizeOpenCodeMessageId(latest.message) : null,
10865
+ latestAssistantMessageId: latestAssistant ? normalizeOpenCodeMessageId(latestAssistant.message) : null,
10866
+ latestAssistantCreatedAt: latestAssistant?.createdAt || 0,
10867
+ latestAssistantCompletedAt: latestAssistant?.completedAt || 0
10868
+ };
10793
10869
  }
10794
10870
  function summarizeOpenCodeMessages(messages = [], { snippetLength = 160, maxMessages = 50 } = {}) {
10795
10871
  const list = Array.isArray(messages) ? messages.slice(0, maxMessages) : [];
@@ -10929,8 +11005,9 @@ async function probeOpenCodeSessionControl(client, { directory, agent, omitAgent
10929
11005
  };
10930
11006
  }
10931
11007
  function openCodeMessageText(message) {
10932
- const parts = [message?.text, message?.content, message?.message, message?.body?.text, message?.data?.text];
10933
- const partList = Array.isArray(message?.parts) ? message.parts : Array.isArray(message?.body?.parts) ? message.body.parts : [];
11008
+ const payload = openCodeMessagePayload(message);
11009
+ const parts = [payload?.text, payload?.content, payload?.message, payload?.body?.text, payload?.data?.text];
11010
+ const partList = Array.isArray(payload?.parts) ? payload.parts : Array.isArray(payload?.body?.parts) ? payload.body.parts : [];
10934
11011
  for (const part of partList) parts.push(part?.text, part?.content);
10935
11012
  return parts.filter((value) => typeof value === "string").join("\n");
10936
11013
  }
@@ -11148,6 +11225,7 @@ async function cleanupManagedClickUpWebhook(entry = {}, { timeoutMs = CLICKUP_WE
11148
11225
  entry.cleaning = true;
11149
11226
  const cleanup = (async () => {
11150
11227
  clickUpWebhookLifecycleLog(entry.worktree, { type: "cleanup_started", reason, key: entry.key, webhookId: entry.state?.webhookId });
11228
+ if (entry.assignmentWatchdog?.interval) clearInterval(entry.assignmentWatchdog.interval);
11151
11229
  const closeResult = await closeClickUpWebhookServer(entry.listener?.server);
11152
11230
  if (entry.listenerRegistry && entry.listener?.key) entry.listenerRegistry.delete(entry.listener.key);
11153
11231
  const preservedResult = { ok: true, skipped: true, reason: "remote_webhook_preserved" };
@@ -11161,10 +11239,10 @@ async function cleanupManagedClickUpWebhook(entry = {}, { timeoutMs = CLICKUP_WE
11161
11239
  if (result?.timeout) clickUpWebhookLifecycleLog(entry.worktree, { type: "cleanup_timeout", reason, webhookId: entry.state?.webhookId });
11162
11240
  return result;
11163
11241
  }
11164
- function registerClickUpWebhookLifecycle({ config, state, worktree, clickupClient, listener, listenerRegistry = activeClickUpWebhookListeners } = {}) {
11242
+ function registerClickUpWebhookLifecycle({ config, state, worktree, clickupClient, listener, listenerRegistry = activeClickUpWebhookListeners, assignmentWatchdog = null } = {}) {
11165
11243
  if (!isClickUpWebhookStateActive(state, config)) return null;
11166
11244
  const key = managedClickUpWebhookKey({ worktree, state, config });
11167
- const entry = { key, config, state, worktree, clickupClient, listener, listenerRegistry, registeredAt: (/* @__PURE__ */ new Date()).toISOString() };
11245
+ const entry = { key, config, state, worktree, clickupClient, listener, listenerRegistry, assignmentWatchdog, registeredAt: (/* @__PURE__ */ new Date()).toISOString() };
11168
11246
  activeClickUpWebhookLifecycleRegistry.set(key, entry);
11169
11247
  installClickUpWebhookSignalHandlers();
11170
11248
  clickUpWebhookLifecycleLog(worktree, { type: "lifecycle_registered", key, webhookId: state.webhookId });
@@ -11454,19 +11532,54 @@ function scheduleClickUpStartupReconciliation({ config, state = {}, worktree = p
11454
11532
  timer?.unref?.();
11455
11533
  return { scheduled: true, delayMs: normalizedDelayMs, timer };
11456
11534
  }
11457
- 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 } = {}) {
11458
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
11535
+ function scheduleClickUpAssignmentWatchdog({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, scheduler = setInterval, intervalMs = config?.opencode?.assignmentWatchdogIntervalMs ?? CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS, saveState = null, reconcile = reconcileClickUpStartup } = {}) {
11536
+ const normalizedIntervalMs = normalizeNonNegativeInteger(intervalMs, CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS);
11537
+ if (!normalizedIntervalMs) {
11538
+ clickUpWebhookLifecycleLog(worktree, { type: "assignment_watchdog_disabled", webhookId: state?.webhookId || null });
11539
+ return { scheduled: false, intervalMs: 0, reason: "disabled" };
11540
+ }
11541
+ let running = false;
11542
+ const run = () => {
11543
+ if (running) {
11544
+ appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_tick_skipped", reason: "previous_tick_running" });
11545
+ return;
11546
+ }
11547
+ running = true;
11548
+ const latestState = readClickUpWebhookState(worktree, config);
11549
+ Promise.resolve().then(() => reconcile({
11550
+ config,
11551
+ state: latestState?.webhookId ? latestState : state,
11552
+ worktree,
11553
+ clickupClient,
11554
+ openCodeClient,
11555
+ saveState,
11556
+ assignmentMode: "watchdog"
11557
+ })).catch((error) => {
11558
+ clickUpWebhookLifecycleLog(worktree, { type: "assignment_watchdog_failed", message: error.message });
11559
+ appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_failed", message: error.message });
11560
+ }).finally(() => {
11561
+ running = false;
11562
+ });
11563
+ };
11564
+ const interval = scheduler(run, normalizedIntervalMs);
11565
+ interval?.unref?.();
11566
+ clickUpWebhookLifecycleLog(worktree, { type: "assignment_watchdog_scheduled", intervalMs: normalizedIntervalMs, webhookId: state?.webhookId || null });
11567
+ return { scheduled: true, intervalMs: normalizedIntervalMs, interval };
11568
+ }
11569
+ 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, assignmentMode = "startup" } = {}) {
11570
+ const watchdogMode = assignmentMode === "watchdog";
11571
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_started" : "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
11459
11572
  if (!config || !clickupClient?.listAssignedTasks) {
11460
11573
  const skipped = { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
11461
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...skipped });
11574
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_finished" : "startup_reconciliation_finished", ...skipped });
11462
11575
  return skipped;
11463
11576
  }
11464
11577
  const readiness = await waitForReadiness(openCodeClient, { worktree, now, opencodeBaseUrl: config.opencode?.baseUrl });
11465
11578
  appendClickUpWebhookLocalLog(worktree, { type: readiness.ok ? "startup_reconciliation_readiness_ready" : "startup_reconciliation_readiness_failed", ...readiness });
11466
11579
  if (!readiness.ok) {
11467
11580
  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: [] } };
11468
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_failed", reason: failed.reason, readiness });
11469
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...failed });
11581
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_failed" : "startup_reconciliation_failed", reason: failed.reason, readiness });
11582
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_finished" : "startup_reconciliation_finished", ...failed });
11470
11583
  return failed;
11471
11584
  }
11472
11585
  let authorizedUserId = "";
@@ -11484,6 +11597,7 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11484
11597
  if (saveState) saveState(nextState);
11485
11598
  };
11486
11599
  const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
11600
+ const startupCommentStartMs = lastWebhookMs || Math.max(0, now().getTime() - CLICKUP_WEBHOOK_STARTUP_COMMENT_LOOKBACK_MS);
11487
11601
  const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
11488
11602
  const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0, undelivered: 0, tasks: [] };
11489
11603
  const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
@@ -11502,9 +11616,29 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11502
11616
  routed.ignored += 1;
11503
11617
  continue;
11504
11618
  }
11619
+ if (watchdogMode) {
11620
+ const existingMetadata = clickUpTaskAgentMetadata(task, config.routing.metadataFieldId);
11621
+ const metadata = normalizeAgentMetadataJson(existingMetadata);
11622
+ const rawSessionId = getNestedMetadataValue(metadata, config.routing.metadataKey);
11623
+ const sessionId = typeof rawSessionId === "string" && rawSessionId.startsWith("ses_") ? rawSessionId : "";
11624
+ const directory = String(getNestedMetadataValue(metadata, "task.worktree") || "").trim();
11625
+ if (sessionId) {
11626
+ try {
11627
+ const activity = await inspectOpenCodeSessionActivity(openCodeClient, { sessionId, directory });
11628
+ appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_session_activity", taskId, sessionId, directory: directory || null, ...activity });
11629
+ if (activity.running) {
11630
+ routed.ignored += 1;
11631
+ routed.tasks.push({ taskId, action: "ignored", ok: true, sessionId, branch: getNestedMetadataValue(metadata, "task.branch") || null, worktree: directory || null, verification: "session_running", delivered: false });
11632
+ continue;
11633
+ }
11634
+ } catch (error) {
11635
+ appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_session_activity_failed", taskId, sessionId, directory: directory || null, message: error.message });
11636
+ }
11637
+ }
11638
+ }
11505
11639
  try {
11506
11640
  const result = await routeClickUpWebhookEvent({
11507
- payload: { webhook_id: mutableState.webhookId || "startup", event: "taskAssigneeUpdated", task_id: taskId, task, startup_reconciliation: true },
11641
+ payload: { webhook_id: mutableState.webhookId || "startup", event: "taskAssigneeUpdated", task_id: taskId, task, startup_reconciliation: true, assignment_watchdog: watchdogMode },
11508
11642
  config,
11509
11643
  state: mutableState,
11510
11644
  worktree,
@@ -11554,12 +11688,12 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11554
11688
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: "error", sessionId: null, reason: "startup_reconciliation_task_failed" });
11555
11689
  await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "startup_reconciliation_task_failed", source: "startup_reconciliation_task" });
11556
11690
  }
11557
- if (!lastWebhookMs || !clickupClient?.getTaskComments) continue;
11691
+ if (!startupCommentStartMs || !clickupClient?.getTaskComments) continue;
11558
11692
  try {
11559
- const commentsResponse = await clickupClient.getTaskComments({ taskId, start: lastWebhookMs, limit: CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT });
11693
+ const commentsResponse = await clickupClient.getTaskComments({ taskId, start: startupCommentStartMs, limit: CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT });
11560
11694
  for (const comment of clickUpCommentListItems(commentsResponse).slice(0, CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT)) {
11561
11695
  const commentMs = clickUpCommentUpdatedMs(comment);
11562
- if (!commentMs || commentMs <= lastWebhookMs) continue;
11696
+ if (!commentMs || commentMs <= startupCommentStartMs) continue;
11563
11697
  const authorId = clickUpCommentAuthorId(comment);
11564
11698
  if (authorId && (authorId === config.routing.ignoredCommentAuthorId || authorId === authorizedUserId)) continue;
11565
11699
  if (!clickUpCommentMentionsProductManager(comment, config.routing)) continue;
@@ -11586,11 +11720,11 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11586
11720
  await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "startup_reconciliation_comments_failed", source: "startup_reconciliation_comments" });
11587
11721
  }
11588
11722
  }
11589
- const emptyPromptSessions = routed.tasks.filter((task) => task.delivered === false && task.sessionId);
11723
+ const emptyPromptSessions = routed.tasks.filter((task) => task.delivered === false && task.sessionId && task.verification !== "session_running");
11590
11724
  if (emptyPromptSessions.length > 0) {
11591
11725
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_validation_failed", tasks: emptyPromptSessions });
11592
11726
  }
11593
- clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...routed });
11727
+ clickUpWebhookLifecycleLog(worktree, { type: watchdogMode ? "assignment_watchdog_finished" : "startup_reconciliation_finished", ...routed });
11594
11728
  return { ok: routed.errors === 0 && routed.undelivered === 0, ...routed, validation: { undelivered: routed.undelivered, emptyPromptSessions } };
11595
11729
  }
11596
11730
  function clickUpWebhookExpectedPath(config) {
@@ -11600,17 +11734,49 @@ function clickUpWebhookExpectedPath(config) {
11600
11734
  return "/";
11601
11735
  }
11602
11736
  }
11603
- async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date(), sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery } = {}) {
11737
+ function summarizeClickUpRouteForLog(result = {}, payload = {}) {
11738
+ return {
11739
+ taskId: result.taskId || clickUpTaskIdFromPayload(payload || {}) || null,
11740
+ event: clickUpEventType(payload || {}) || null,
11741
+ eventKey: result.eventKey || clickUpWebhookEventKey(payload || {}) || null,
11742
+ ok: result.ok === true,
11743
+ action: result.action || null,
11744
+ reason: result.reason || null,
11745
+ sessionId: result.sessionId || result.replacementSessionId || null,
11746
+ branch: result.branch || null,
11747
+ worktree: result.worktree || null
11748
+ };
11749
+ }
11750
+ function runClickUpWebhookRouteInBackground({ payload, config, state, worktree, clickupClient, openCodeClient, saveState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, now } = {}) {
11751
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11752
+ appendClickUpWebhookLocalLog(worktree, { type: "webhook_route_started", taskId: clickUpTaskIdFromPayload(payload || {}) || null, event: clickUpEventType(payload || {}) || null, eventKey: clickUpWebhookEventKey(payload || {}) || null, async: true, startedAt });
11753
+ const route = Promise.resolve().then(() => routeClickUpWebhookEvent({ payload, config, state, worktree, clickupClient, openCodeClient, saveState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, now })).then((result) => {
11754
+ appendClickUpWebhookLocalLog(worktree, { type: "webhook_route_finished", async: true, ...summarizeClickUpRouteForLog(result, payload) });
11755
+ return result;
11756
+ }).catch((error) => {
11757
+ appendClickUpWebhookLocalLog(worktree, { type: "webhook_route_failed", taskId: clickUpTaskIdFromPayload(payload || {}) || null, event: clickUpEventType(payload || {}) || null, eventKey: clickUpWebhookEventKey(payload || {}) || null, async: true, message: error.message });
11758
+ return { ok: false, action: "error", reason: "background_route_failed", message: error.message };
11759
+ });
11760
+ route.catch(() => {
11761
+ });
11762
+ return route;
11763
+ }
11764
+ async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date(), sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, asyncRouting = false } = {}) {
11604
11765
  let payload = null;
11605
11766
  let handled = null;
11606
11767
  let authenticatedWebhook = false;
11607
11768
  let receivedAt = null;
11608
11769
  let activeState = state;
11770
+ let latestPersistedState = state;
11771
+ const persistState = (nextState) => {
11772
+ latestPersistedState = nextState;
11773
+ if (saveState) saveState(nextState);
11774
+ };
11609
11775
  const finish = (result) => {
11610
11776
  handled = result;
11611
11777
  receivedAt ??= now();
11612
- const auditState = authenticatedWebhook ? { ...activeState, lastWebhookAt: receivedAt.toISOString() } : activeState;
11613
- if (saveState && authenticatedWebhook) saveState(auditState);
11778
+ const auditState = authenticatedWebhook ? { ...latestPersistedState, lastWebhookAt: receivedAt.toISOString() } : latestPersistedState;
11779
+ if (saveState && authenticatedWebhook) persistState(auditState);
11614
11780
  writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state: auditState, handled, payload, at: receivedAt });
11615
11781
  return result;
11616
11782
  };
@@ -11637,7 +11803,12 @@ async function handleClickUpWebhookRequest({ method = "POST", url = null, header
11637
11803
  return finish({ ok: false, status: 400, reason: "invalid_json" });
11638
11804
  }
11639
11805
  const receivedState = { ...activeState, lastWebhookAt: receivedAt.toISOString() };
11640
- const result = await routeClickUpWebhookEvent({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery });
11806
+ persistState(receivedState);
11807
+ if (asyncRouting) {
11808
+ runClickUpWebhookRouteInBackground({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState: persistState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, now });
11809
+ return finish({ ok: true, status: 200, reason: "accepted", result: { action: "accepted", taskId: clickUpTaskIdFromPayload(payload), eventKey: clickUpWebhookEventKey(payload) } });
11810
+ }
11811
+ const result = await routeClickUpWebhookEvent({ payload, config, state: receivedState, worktree, clickupClient, openCodeClient, saveState: persistState, sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery });
11641
11812
  return finish({ ok: result.ok, status: result.ok ? 200 : 422, result });
11642
11813
  } catch (error) {
11643
11814
  writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state, handled, error, payload, at: now() });
@@ -11714,7 +11885,8 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
11714
11885
  worktree,
11715
11886
  clickupClient,
11716
11887
  openCodeClient,
11717
- saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, config)
11888
+ saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, config),
11889
+ asyncRouting: true
11718
11890
  });
11719
11891
  res.writeHead(handled.status, { "Content-Type": "application/json" });
11720
11892
  res.end(JSON.stringify({ ok: handled.ok, reason: handled.reason, result: handled.result?.action }));
@@ -12866,7 +13038,17 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
12866
13038
  ...listenerState,
12867
13039
  listener: { bindHost: clickUpWebhookValidation.config.webhook.bindHost, bindPort: clickUpWebhookValidation.config.webhook.bindPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
12868
13040
  }, clickUpWebhookValidation.config);
12869
- registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry });
13041
+ const assignmentWatchdog = scheduleClickUpAssignmentWatchdog({
13042
+ config: clickUpWebhookValidation.config,
13043
+ state: activeState,
13044
+ worktree,
13045
+ clickupClient: lifecycleClickUpClient,
13046
+ openCodeClient: input.client,
13047
+ scheduler: input.assignmentWatchdogScheduler,
13048
+ intervalMs: input.assignmentWatchdogIntervalMs,
13049
+ saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, clickUpWebhookValidation.config)
13050
+ });
13051
+ registerClickUpWebhookLifecycle({ config: clickUpWebhookValidation.config, state: activeState, worktree, clickupClient: lifecycleClickUpClient, listener: readyListener, listenerRegistry, assignmentWatchdog });
12870
13052
  scheduleClickUpStartupReconciliation({
12871
13053
  config: clickUpWebhookValidation.config,
12872
13054
  state: activeState,
@@ -13522,7 +13704,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
13522
13704
  }
13523
13705
  };
13524
13706
  }
13525
- 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, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
13707
+ 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, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
13526
13708
 
13527
13709
  // src/sanitize_cli.js
13528
13710
  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.66",
3
+ "version": "0.1.68",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+ssh://git@github.com/defend-tech/opencode-optima.git"