@defend-tech/opencode-optima 0.1.47 → 0.1.49

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
@@ -18,7 +18,7 @@
18
18
  - `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
19
  - 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
20
  - WPM estimates `Story Points` during `plan` and re-estimates on material plan changes.
21
- - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and code instead of personal names.
21
+ - 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
22
  - ClickUp status actions: `backlog` ignore, `plan` plan plus remove the PM assignee before assigning `CTO`/`PO` at plan end, `in progress` execute, `validation` Tech Lead + Validator/QA gates. After validation, Validator/QA may merge validated subtasks into the parent branch without `CTO`/`PO` approval; validated parent tasks stay in `validation` for `CTO`/`PO` approval, they approve by moving the parent to `merge`, and Validator/QA then attempts the parent merge into `dev`. `completed`/`Closed` ignore unless reopened.
23
23
  - One shared-worktree `implementation` task may be active; ClickUp-first delivery should use task-specific worktrees/branches.
24
24
  - Agent messages must start with `[Agent Message] From: <agent_name> To: <agent_name>`.
@@ -20,7 +20,7 @@
20
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 become routed ClickUp tasks.
21
21
  - 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
22
  - WPM estimates `Story Points` during `plan` and re-estimates on material plan changes.
23
- - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and code instead of personal names.
23
+ - 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
24
  - ClickUp-first statuses: `backlog` ignore, `plan` plan plus remove the PM assignee before assigning `CTO`/`PO` at plan end, `in progress` execute, `validation` Tech Lead + Validator/QA gates. Validator/QA may merge validated subtasks into the parent branch without `CTO`/`PO` approval; validated parent tasks stay in `validation` for `CTO`/`PO` approval, they approve by moving the parent to `merge`, and Validator/QA then attempts the parent merge into `dev`. `completed`/`Closed` ignore unless reopened.
25
25
  - Signed agent-to-agent messages must start exactly: `[Agent Message] From: <agent_name> To: <agent_name>`.
26
26
  - Direct all clarifications, blockers, and specialist questions through PMA unless explicitly in a direct discussion-capable role.
@@ -41,7 +41,7 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
41
41
  ## Status Actions
42
42
 
43
43
  - `backlog`: ignore until prioritized.
44
- - Human registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role IDs in workflow text, ClickUp comments, and automation config.
44
+ - Human registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if the task worktree lacks that file, use the Optima-provided Human Role Fallback Registry and configured ClickUp IDs instead of blocking solely on the missing repo-local file. Use role IDs in workflow text, ClickUp comments, and automation config.
45
45
  - `plan`: clarify AC/SCR/test strategy with Validator/QA, decompose, create/update Definition, estimate Story Points, hand off implementation, remove PM assignee first, then assign `CTO` + `PO` or next owner; target zero PM-assigned tasks.
46
46
  - `in progress`: execute through assigned delivery agent or workflow runner.
47
47
  - `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 parents stay in `validation`, assigned to `CTO`/`PO`, ready for approval.
package/dist/index.js CHANGED
@@ -8130,6 +8130,42 @@ function resolveHumanRoles(roles = CLICKUP_FINAL_APPROVER_ROLES, registry = load
8130
8130
  function finalApprovalAssignees(roles = CLICKUP_FINAL_APPROVER_ROLES, registry = loadHumansRegistry()) {
8131
8131
  return resolveHumanRoles(roles, registry);
8132
8132
  }
8133
+ function normalizedHumanRoleRegistry(registry = loadHumansRegistry()) {
8134
+ return Object.fromEntries(
8135
+ Object.entries(registry || {}).map(([role, human]) => [String(role || "").trim(), String(human || "").trim()]).filter(([role, human]) => role && human)
8136
+ );
8137
+ }
8138
+ function normalizeClickUpRoleIds(raw = {}) {
8139
+ const source = isPlainObject(raw) ? raw : {};
8140
+ const entries = Object.entries(source).map(([role, id]) => [String(role || "").trim(), String(id || "").trim()]);
8141
+ return Object.fromEntries(entries.filter(([role, id]) => role && id));
8142
+ }
8143
+ function buildHumanRoleContext({ roles = CLICKUP_FINAL_APPROVER_ROLES, registry = loadHumansRegistry(), roleIds = {} } = {}) {
8144
+ const normalizedRegistry = normalizedHumanRoleRegistry(registry);
8145
+ const normalizedIds = normalizeClickUpRoleIds(roleIds);
8146
+ return [...new Set(roles.map((role) => String(role || "").trim()).filter(Boolean))].map((role) => ({
8147
+ role,
8148
+ human: normalizedRegistry[role] || "",
8149
+ clickupUserId: normalizedIds[role] || ""
8150
+ }));
8151
+ }
8152
+ function formatHumanRoleContext(context = []) {
8153
+ const entries = Array.isArray(context) ? context : [];
8154
+ if (!entries.length) return "";
8155
+ return entries.map((entry) => {
8156
+ const human = entry.human ? ` = ${entry.human}` : "";
8157
+ const id = entry.clickupUserId ? ` (ClickUp ID: ${entry.clickupUserId})` : "";
8158
+ return `- ${entry.role}${human}${id}`;
8159
+ }).join("\n");
8160
+ }
8161
+ function hasActionableClickUpRoute(result = {}) {
8162
+ if (!result?.ok || result.action === "ignored") return false;
8163
+ if (!result.sessionId) return false;
8164
+ if (result.action === "message_delivery_failed" || result.action === "error") return false;
8165
+ if (result.deliveryVerification?.ok === false) return false;
8166
+ if (result.deliveryVerification?.method === "prompt_admission") return false;
8167
+ return true;
8168
+ }
8133
8169
  function determineClickUpMergeAuthority({ isSubtask = false, clickupStatus = "", validationPassed = false, mergeFailed = false, finalApprovalRoles = CLICKUP_FINAL_APPROVER_ROLES, humansRegistry } = {}) {
8134
8170
  if (mergeFailed) {
8135
8171
  return {
@@ -9015,7 +9051,8 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9015
9051
  ignoredStatuses,
9016
9052
  metadataFieldId: String(routing.metadata_field_id || routing.metadataFieldId || "").trim(),
9017
9053
  metadataKey: String(routing.metadata_key || routing.metadataKey || CLICKUP_PM_METADATA_KEY).trim(),
9018
- ignoredCommentAuthorId: String(routing.ignored_comment_author_id || routing.ignoredCommentAuthorId || routing.product_manager_mention_user_id || routing.productManagerMentionUserId || "").trim()
9054
+ ignoredCommentAuthorId: String(routing.ignored_comment_author_id || routing.ignoredCommentAuthorId || routing.product_manager_mention_user_id || routing.productManagerMentionUserId || "").trim(),
9055
+ humanRoleClickUpIds: normalizeClickUpRoleIds(routing.human_role_clickup_ids || routing.humanRoleClickUpIds || routing.role_ids || routing.roleIds)
9019
9056
  }
9020
9057
  };
9021
9058
  const errors = [];
@@ -9619,6 +9656,22 @@ async function createOpenCodeSession(client, { title, directory, agent } = {}) {
9619
9656
  }
9620
9657
  throw firstError || new Error("OpenCode session create failed.");
9621
9658
  }
9659
+ async function waitForOpenCodeReadiness(client, { worktree = process.cwd(), attempts = 10, delayMs = 500, now = () => /* @__PURE__ */ new Date() } = {}) {
9660
+ if (typeof client?.session?.create !== "function") return { ok: true, skipped: true, reason: "session_create_probe_unavailable" };
9661
+ let lastError = "opencode_not_ready";
9662
+ const maxAttempts = Math.max(1, Number(attempts) || 1);
9663
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
9664
+ try {
9665
+ const sessionId = await createOpenCodeSession(client, { title: `Optima startup readiness probe ${now().toISOString()}`, directory: worktree });
9666
+ if (await openCodeSessionExists(client, sessionId)) return { ok: true, method: "session_create_probe", sessionId, attempts: attempt };
9667
+ lastError = "readiness_probe_session_not_visible";
9668
+ } catch (error) {
9669
+ lastError = error.message || "opencode_not_ready";
9670
+ }
9671
+ if (attempt < maxAttempts && delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
9672
+ }
9673
+ return { ok: false, reason: lastError, attempts: maxAttempts };
9674
+ }
9622
9675
  function assertOpenCodePromptAccepted(result) {
9623
9676
  const status = Number(result?.status || result?.response?.status || result?.error?.status || 0);
9624
9677
  if (status >= 400 || result?.ok === false || result?.error) {
@@ -9656,13 +9709,13 @@ async function readOpenCodeJsonResponse(response, endpointName) {
9656
9709
  return { raw };
9657
9710
  }
9658
9711
  }
9659
- async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, fetchImpl = globalThis.fetch } = {}) {
9712
+ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
9660
9713
  if (typeof fetchImpl !== "function") throw new Error("OpenCode direct prompt delivery requires fetch.");
9661
9714
  const root = normalizeOpenCodeBaseUrl(baseUrl, "");
9662
9715
  if (!root) throw new Error("OpenCode direct prompt delivery requires a base URL.");
9663
9716
  const encodedSession = encodeURIComponent(sessionId);
9664
9717
  const attempts = [
9665
- {
9718
+ legacyOnly ? null : {
9666
9719
  name: "v2 prompt",
9667
9720
  url: `${root}/api/session/${encodedSession}/prompt`,
9668
9721
  body: { prompt: { text }, delivery: "queue", resume: true },
@@ -9681,7 +9734,7 @@ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent,
9681
9734
  return { ok: true, method: "http", endpoint: "/session/{sessionID}/prompt_async", status: response.status, data: data?.data || null, response: data };
9682
9735
  }
9683
9736
  }
9684
- ];
9737
+ ].filter(Boolean);
9685
9738
  let firstError = null;
9686
9739
  for (const attempt of attempts) {
9687
9740
  const response = await fetchImpl(attempt.url, {
@@ -9715,7 +9768,7 @@ async function callOpenCodePromptWithFallbacks(method, sessionId, flatPayload, s
9715
9768
  }
9716
9769
  throw firstError;
9717
9770
  }
9718
- async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false } = {}) {
9771
+ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false } = {}) {
9719
9772
  const directBaseUrl = opencodeBaseUrl || baseUrl;
9720
9773
  const parts = [{ type: "text", text }];
9721
9774
  const flatPayload = { directory, agent, parts };
@@ -9739,7 +9792,7 @@ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, direct
9739
9792
  firstError ??= error;
9740
9793
  }
9741
9794
  }
9742
- if (directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, fetchImpl });
9795
+ if (directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, fetchImpl, legacyOnly });
9743
9796
  if (firstError) throw firstError;
9744
9797
  throw new Error("OpenCode client does not expose session.prompt or session.promptAsync.");
9745
9798
  }
@@ -9774,18 +9827,20 @@ function openCodeMessageText(message) {
9774
9827
  for (const part of partList) parts.push(part?.text, part?.content);
9775
9828
  return parts.filter((value) => typeof value === "string").join("\n");
9776
9829
  }
9777
- async function verifyOpenCodeSessionEventDelivery(client, { sessionId, beforeMessages = null, expectedText = "", markers = [], attempts = 3, delayMs = 25 } = {}) {
9830
+ async function verifyOpenCodeSessionEventDelivery(client, { sessionId, beforeMessages = null, expectedText = "", markers = [], attempts = 8, delayMs = 250 } = {}) {
9778
9831
  let lastError = "message_verification_unavailable";
9779
9832
  for (let attempt = 0; attempt < Math.max(1, attempts); attempt += 1) {
9780
9833
  try {
9781
9834
  const afterMessages = await readOpenCodeSessionMessages(client, { sessionId, limit: 50 });
9782
- if (!afterMessages) return { ok: true, method: "verification_unavailable", skipped: true };
9835
+ if (!afterMessages) return { ok: false, reason: "message_verification_unavailable" };
9783
9836
  const beforeCount = Array.isArray(beforeMessages) ? beforeMessages.length : null;
9784
- if (beforeCount !== null && afterMessages.length > beforeCount) return { ok: true, method: "message_count", beforeCount, afterCount: afterMessages.length };
9837
+ const lastMessage = afterMessages.at(-1) || null;
9838
+ const lastMessageId = lastMessage?.id || lastMessage?.messageID || lastMessage?.messageId || null;
9839
+ if (beforeCount !== null && afterMessages.length > beforeCount) return { ok: true, method: "message_count", beforeCount, afterCount: afterMessages.length, lastMessageId };
9785
9840
  const textNeedles = [expectedText, ...markers].map((value) => String(value || "").trim()).filter(Boolean);
9786
9841
  const haystack = afterMessages.slice(-20).map(openCodeMessageText).join("\n");
9787
9842
  const matched = textNeedles.find((needle) => haystack.includes(needle));
9788
- if (matched) return { ok: true, method: "message_text", marker: matched, beforeCount, afterCount: afterMessages.length };
9843
+ if (matched) return { ok: true, method: "message_text", marker: matched, beforeCount, afterCount: afterMessages.length, lastMessageId };
9789
9844
  lastError = "message_not_visible";
9790
9845
  } catch (error) {
9791
9846
  lastError = error.message || "message_verification_failed";
@@ -9820,21 +9875,28 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
9820
9875
  const blocker2 = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: error.message, source: "delivery_admission_failed" });
9821
9876
  return { ok: false, action: "message_delivery_failed", reason: error.message, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker2 };
9822
9877
  }
9823
- if (admissionVerification) return { ok: true, verification: admissionVerification, fallback: false };
9824
9878
  let verification = await verifySessionEventDelivery(openCodeClient, { sessionId, beforeMessages, expectedText: text, markers: eventMarkers });
9825
- if (verification?.ok) return { ok: true, verification, fallback: false };
9879
+ if (verification?.ok) return { ok: true, verification, admissionVerification, fallback: false };
9880
+ if (verification?.reason === "message_verification_unavailable" && !admissionVerification) {
9881
+ return { ok: true, verification: { ok: true, method: "legacy_prompt_accepted", skipped: true }, fallback: false };
9882
+ }
9883
+ if (admissionVerification) {
9884
+ appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_admitted_but_invisible", taskId, sessionId, admission: admissionVerification, reason: verification?.reason || "message_not_visible" });
9885
+ }
9826
9886
  const canFallbackDirect = Boolean(opencodeBaseUrl);
9827
9887
  if (canFallbackDirect) {
9828
9888
  const retryBeforeMessages = await readOpenCodeSessionMessages(openCodeClient, { sessionId, limit: 50 }).catch(() => beforeMessages);
9829
- const retrySendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: true });
9889
+ const retrySendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: true, legacyOnly: Boolean(admissionVerification) });
9830
9890
  try {
9831
9891
  admissionVerification = openCodePromptAdmissionVerification(retrySendResult, sessionId);
9832
9892
  } catch (error) {
9833
9893
  verification = { ok: false, reason: error.message };
9834
9894
  }
9835
- if (admissionVerification) return { ok: true, verification: admissionVerification, fallback: true };
9836
9895
  verification = await verifySessionEventDelivery(openCodeClient, { sessionId, beforeMessages: retryBeforeMessages, expectedText: text, markers: eventMarkers });
9837
- if (verification?.ok) return { ok: true, verification, fallback: true };
9896
+ if (verification?.ok) return { ok: true, verification, admissionVerification, fallback: true };
9897
+ if (verification?.reason === "message_verification_unavailable" && !admissionVerification) {
9898
+ return { ok: true, verification: { ok: true, method: "legacy_prompt_accepted", skipped: true }, fallback: true };
9899
+ }
9838
9900
  }
9839
9901
  const reason = verification?.reason || "message_delivery_failed";
9840
9902
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason, fallbackAttempted: canFallbackDirect });
@@ -9880,11 +9942,12 @@ async function recoverClickUpPmSession({ openCodeClient, sendSessionEvent, click
9880
9942
  const replacementMetadata = clearClickUpPendingSessionMetadata(setClickUpSessionMetadata(metadataWithRouting, config.routing.metadataKey, replacementSessionId), config.routing.metadataKey);
9881
9943
  await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: replacementMetadata });
9882
9944
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_succeeded", taskId, staleSessionId, replacementSessionId });
9883
- return { ok: true, action: "sent_to_replacement_session", taskId, sessionId: replacementSessionId, staleSessionId, replacementAttempted: true, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath, deliveryVerification: replacementDelivery.verification, deliveryFallback: replacementDelivery.fallback };
9945
+ return { ok: true, action: "sent_to_replacement_session", taskId, sessionId: replacementSessionId, staleSessionId, replacementAttempted: true, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath, deliveryVerification: replacementDelivery.verification, deliveryAdmission: replacementDelivery.admissionVerification, deliveryFallback: replacementDelivery.fallback, deliveryAttempts: replacementDelivery.fallback ? 2 : 1 };
9884
9946
  }
9885
- function formatClickUpWebhookPrompt({ eventType, taskId, payload, branch = "", worktree = "", deliveryEvidencePath = "" }) {
9947
+ function formatClickUpWebhookPrompt({ eventType, taskId, payload, branch = "", worktree = "", deliveryEvidencePath = "", humanRoleContext = [] }) {
9886
9948
  const comment = clickUpCommentFromPayload(payload);
9887
9949
  const commentText = clickUpCommentText(comment).trim();
9950
+ const humanRoleLines = formatHumanRoleContext(humanRoleContext);
9888
9951
  return [
9889
9952
  "[ClickUp Webhook Event]",
9890
9953
  `Event: ${eventType}`,
@@ -9892,9 +9955,12 @@ function formatClickUpWebhookPrompt({ eventType, taskId, payload, branch = "", w
9892
9955
  branch ? `Branch: ${branch}` : null,
9893
9956
  worktree ? `Worktree: ${worktree}` : null,
9894
9957
  deliveryEvidencePath ? `Delivery evidence path: ${deliveryEvidencePath}` : null,
9958
+ humanRoleLines ? "Human role fallback registry (Optima-provided; non-blocking if repo docs/core/humans.md is missing):" : null,
9959
+ humanRoleLines || null,
9895
9960
  commentText ? `Comment: ${commentText}` : null,
9896
9961
  "",
9897
9962
  "Handle this ClickUp Product Manager event using the ClickUp-first workflow rules. Do not assume broad chat routing; this event passed Optima webhook gates.",
9963
+ "If the task worktree lacks docs/core/humans.md, use the Optima-provided human role fallback registry above instead of blocking solely on that missing file.",
9898
9964
  deliveryEvidencePath ? `Final merge-trackable evidence must be written under ${deliveryEvidencePath}; use .optima only as local mirror/staging.` : null
9899
9965
  ].filter((part) => part !== null).join("\n");
9900
9966
  }
@@ -10145,7 +10211,8 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
10145
10211
  evidence_path: `.optima/evidences/${branchSafeClickUpId(taskId)}/SUMMARY.md`
10146
10212
  };
10147
10213
  const metadataWithRouting = setClickUpTaskRoutingMetadata(existingMetadata, routingMetadata);
10148
- const prompt = formatClickUpWebhookPrompt({ eventType, taskId, payload, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath });
10214
+ const humanRoleContext = buildHumanRoleContext({ roleIds: config.routing?.humanRoleClickUpIds });
10215
+ const prompt = formatClickUpWebhookPrompt({ eventType, taskId, payload, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, humanRoleContext });
10149
10216
  if (!existingSessionId) {
10150
10217
  let pendingSessionId = getNestedMetadataValue(metadata, clickUpPendingSessionKey(config.routing.metadataKey)) || state.pendingSessions?.[taskId];
10151
10218
  if (!pendingSessionId) {
@@ -10171,7 +10238,7 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
10171
10238
  await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: nextMetadata });
10172
10239
  const { [taskId]: _completedPending, ...remainingPending } = stateToPersist.pendingSessions || {};
10173
10240
  stateToPersist = { ...stateToPersist, pendingSessions: remainingPending };
10174
- return finish({ ok: true, action: "created_session", taskId, sessionId: pendingSessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryFallback: delivery.fallback });
10241
+ return finish({ ok: true, action: "created_session", taskId, sessionId: pendingSessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryAdmission: delivery.admissionVerification, deliveryFallback: delivery.fallback, deliveryAttempts: delivery.fallback ? 2 : 1 });
10175
10242
  }
10176
10243
  const sessionId = String(existingSessionId);
10177
10244
  if (await sessionExists(openCodeClient, sessionId)) {
@@ -10181,7 +10248,7 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
10181
10248
  const recovery2 = await recoverClickUpPmSession({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, staleSessionId: sessionId, sessionTitle, taskRoute, metadataWithRouting, config, prompt, eventMarkers: [taskId, eventType], deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, eventKey, createSession, verifySessionEventDelivery });
10182
10249
  return finish(recovery2);
10183
10250
  }
10184
- return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryFallback: delivery.fallback });
10251
+ return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryAdmission: delivery.admissionVerification, deliveryFallback: delivery.fallback, deliveryAttempts: delivery.fallback ? 2 : 1 });
10185
10252
  }
10186
10253
  const at = now().toISOString();
10187
10254
  appendClickUpWebhookLocalLog(worktree, { type: "missing_session", taskId, sessionId, host, at });
@@ -10192,8 +10259,11 @@ async function routeClickUpWebhookEvent(options = {}) {
10192
10259
  const taskId = clickUpTaskIdFromPayload(options.payload || {});
10193
10260
  return withClickUpTaskRouteLock(taskId, () => routeClickUpWebhookEventUnlocked(options));
10194
10261
  }
10195
- async function reconcileClickUpStartup({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, saveState = null, now = () => /* @__PURE__ */ new Date(), limit = CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT } = {}) {
10262
+ 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 } = {}) {
10196
10263
  if (!config || !clickupClient?.listAssignedTasks) return { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
10264
+ const readiness = await waitForReadiness(openCodeClient, { worktree, now });
10265
+ 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: [] } };
10197
10267
  let authorizedUserId = "";
10198
10268
  if (clickupClient?.getAuthorizedUser) {
10199
10269
  try {
@@ -10210,7 +10280,7 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10210
10280
  };
10211
10281
  const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
10212
10282
  const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
10213
- const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0 };
10283
+ const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0, undelivered: 0, tasks: [] };
10214
10284
  clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
10215
10285
  const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
10216
10286
  const tasks = clickUpTaskListItems(listed).slice(0, limit);
@@ -10239,18 +10309,44 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10239
10309
  sessionExists,
10240
10310
  createSession,
10241
10311
  sendSessionEvent,
10312
+ verifySessionEventDelivery,
10242
10313
  saveState: persistState,
10243
10314
  now
10244
10315
  });
10245
- if (result?.ok && result.action !== "ignored") {
10316
+ const routeSummary = {
10317
+ taskId,
10318
+ action: result?.action || "unknown",
10319
+ ok: result?.ok === true,
10320
+ sessionId: result?.sessionId || result?.replacementSessionId || null,
10321
+ branch: result?.branch || null,
10322
+ worktree: result?.worktree || null,
10323
+ verification: result?.deliveryVerification?.method || result?.deliveryVerification?.reason || null,
10324
+ delivered: hasActionableClickUpRoute(result),
10325
+ attempts: result?.deliveryAttempts || null,
10326
+ finalMessageCount: result?.deliveryVerification?.afterCount ?? null,
10327
+ finalMessageId: result?.deliveryVerification?.lastMessageId || null
10328
+ };
10329
+ routed.tasks.push(routeSummary);
10330
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_routed", ...routeSummary });
10331
+ if (routeSummary.delivered) {
10246
10332
  routed.assigned += 1;
10333
+ } else if (result?.ok && result.action === "ignored") {
10334
+ routed.ignored += 1;
10247
10335
  } else if (result?.ok === false) {
10248
10336
  routed.errors += 1;
10337
+ routed.undelivered += 1;
10338
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: result?.action || "error", sessionId: routeSummary.sessionId, reason: result.reason || "startup_reconciliation_route_failed" });
10249
10339
  if (!result.blockerTag) await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: result.reason || "startup_reconciliation_route_failed", source: "startup_reconciliation_task" });
10340
+ } else {
10341
+ routed.undelivered += 1;
10342
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: routeSummary.action, sessionId: routeSummary.sessionId, reason: "route_not_actionable" });
10250
10343
  }
10251
10344
  } catch (error) {
10252
10345
  routed.errors += 1;
10346
+ routed.undelivered += 1;
10347
+ routed.tasks.push({ taskId, action: "error", ok: false, sessionId: null, branch: null, worktree: null, verification: error.message, delivered: false });
10253
10348
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_failed", taskId, message: error.message });
10349
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: "error", sessionId: null, reason: "startup_reconciliation_task_failed" });
10254
10350
  await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "startup_reconciliation_task_failed", source: "startup_reconciliation_task" });
10255
10351
  }
10256
10352
  if (!lastWebhookMs || !clickupClient?.getTaskComments) continue;
@@ -10273,6 +10369,7 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10273
10369
  sessionExists,
10274
10370
  createSession,
10275
10371
  sendSessionEvent,
10372
+ verifySessionEventDelivery,
10276
10373
  saveState: persistState,
10277
10374
  now
10278
10375
  });
@@ -10284,8 +10381,12 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10284
10381
  await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "startup_reconciliation_comments_failed", source: "startup_reconciliation_comments" });
10285
10382
  }
10286
10383
  }
10384
+ const emptyPromptSessions = routed.tasks.filter((task) => task.delivered === false && task.sessionId);
10385
+ if (emptyPromptSessions.length > 0) {
10386
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_validation_failed", tasks: emptyPromptSessions });
10387
+ }
10287
10388
  clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...routed });
10288
- return { ok: routed.errors === 0, ...routed };
10389
+ return { ok: routed.errors === 0 && routed.undelivered === 0, ...routed, validation: { undelivered: routed.undelivered, emptyPromptSessions } };
10289
10390
  }
10290
10391
  function clickUpWebhookExpectedPath(config) {
10291
10392
  try {
@@ -11403,6 +11504,19 @@ ${additionFragment}`;
11403
11504
  delete agentConfig.tools.optima_prompt_workflow;
11404
11505
  }
11405
11506
  }
11507
+ if (id === "workflow_product_manager") {
11508
+ const humanRoleContext = buildHumanRoleContext({ roleIds: options.clickUpWebhookValidation?.config?.routing?.humanRoleClickUpIds });
11509
+ const humanRoleLines = formatHumanRoleContext(humanRoleContext);
11510
+ if (humanRoleLines) {
11511
+ agentConfig.prompt = `${agentConfig.prompt}
11512
+
11513
+ ## Optima Human Role Fallback Registry
11514
+
11515
+ ${humanRoleLines}
11516
+
11517
+ Use this Optima-provided fallback when the current task worktree lacks docs/core/humans.md; missing repo-local humans.md is not a hard blocker when this context or configured ClickUp IDs are present.`;
11518
+ }
11519
+ }
11406
11520
  ourAgents[id] = agentConfig;
11407
11521
  if (repoCfg.features?.debug_dumps !== false) {
11408
11522
  const debugPath = path2.join(debugDir, `${id}.md`);
@@ -12071,7 +12185,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
12071
12185
  }
12072
12186
  };
12073
12187
  }
12074
- 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, 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 };
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 };
12075
12189
  export {
12076
12190
  OptimaPlugin as default
12077
12191
  };
@@ -8137,6 +8137,42 @@ function resolveHumanRoles(roles = CLICKUP_FINAL_APPROVER_ROLES, registry = load
8137
8137
  function finalApprovalAssignees(roles = CLICKUP_FINAL_APPROVER_ROLES, registry = loadHumansRegistry()) {
8138
8138
  return resolveHumanRoles(roles, registry);
8139
8139
  }
8140
+ function normalizedHumanRoleRegistry(registry = loadHumansRegistry()) {
8141
+ return Object.fromEntries(
8142
+ Object.entries(registry || {}).map(([role, human]) => [String(role || "").trim(), String(human || "").trim()]).filter(([role, human]) => role && human)
8143
+ );
8144
+ }
8145
+ function normalizeClickUpRoleIds(raw = {}) {
8146
+ const source = isPlainObject(raw) ? raw : {};
8147
+ const entries = Object.entries(source).map(([role, id]) => [String(role || "").trim(), String(id || "").trim()]);
8148
+ return Object.fromEntries(entries.filter(([role, id]) => role && id));
8149
+ }
8150
+ function buildHumanRoleContext({ roles = CLICKUP_FINAL_APPROVER_ROLES, registry = loadHumansRegistry(), roleIds = {} } = {}) {
8151
+ const normalizedRegistry = normalizedHumanRoleRegistry(registry);
8152
+ const normalizedIds = normalizeClickUpRoleIds(roleIds);
8153
+ return [...new Set(roles.map((role) => String(role || "").trim()).filter(Boolean))].map((role) => ({
8154
+ role,
8155
+ human: normalizedRegistry[role] || "",
8156
+ clickupUserId: normalizedIds[role] || ""
8157
+ }));
8158
+ }
8159
+ function formatHumanRoleContext(context = []) {
8160
+ const entries = Array.isArray(context) ? context : [];
8161
+ if (!entries.length) return "";
8162
+ return entries.map((entry) => {
8163
+ const human = entry.human ? ` = ${entry.human}` : "";
8164
+ const id = entry.clickupUserId ? ` (ClickUp ID: ${entry.clickupUserId})` : "";
8165
+ return `- ${entry.role}${human}${id}`;
8166
+ }).join("\n");
8167
+ }
8168
+ function hasActionableClickUpRoute(result = {}) {
8169
+ if (!result?.ok || result.action === "ignored") return false;
8170
+ if (!result.sessionId) return false;
8171
+ if (result.action === "message_delivery_failed" || result.action === "error") return false;
8172
+ if (result.deliveryVerification?.ok === false) return false;
8173
+ if (result.deliveryVerification?.method === "prompt_admission") return false;
8174
+ return true;
8175
+ }
8140
8176
  function determineClickUpMergeAuthority({ isSubtask = false, clickupStatus = "", validationPassed = false, mergeFailed = false, finalApprovalRoles = CLICKUP_FINAL_APPROVER_ROLES, humansRegistry } = {}) {
8141
8177
  if (mergeFailed) {
8142
8178
  return {
@@ -9022,7 +9058,8 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9022
9058
  ignoredStatuses,
9023
9059
  metadataFieldId: String(routing.metadata_field_id || routing.metadataFieldId || "").trim(),
9024
9060
  metadataKey: String(routing.metadata_key || routing.metadataKey || CLICKUP_PM_METADATA_KEY).trim(),
9025
- ignoredCommentAuthorId: String(routing.ignored_comment_author_id || routing.ignoredCommentAuthorId || routing.product_manager_mention_user_id || routing.productManagerMentionUserId || "").trim()
9061
+ ignoredCommentAuthorId: String(routing.ignored_comment_author_id || routing.ignoredCommentAuthorId || routing.product_manager_mention_user_id || routing.productManagerMentionUserId || "").trim(),
9062
+ humanRoleClickUpIds: normalizeClickUpRoleIds(routing.human_role_clickup_ids || routing.humanRoleClickUpIds || routing.role_ids || routing.roleIds)
9026
9063
  }
9027
9064
  };
9028
9065
  const errors = [];
@@ -9626,6 +9663,22 @@ async function createOpenCodeSession(client, { title, directory, agent } = {}) {
9626
9663
  }
9627
9664
  throw firstError || new Error("OpenCode session create failed.");
9628
9665
  }
9666
+ async function waitForOpenCodeReadiness(client, { worktree = process.cwd(), attempts = 10, delayMs = 500, now = () => /* @__PURE__ */ new Date() } = {}) {
9667
+ if (typeof client?.session?.create !== "function") return { ok: true, skipped: true, reason: "session_create_probe_unavailable" };
9668
+ let lastError = "opencode_not_ready";
9669
+ const maxAttempts = Math.max(1, Number(attempts) || 1);
9670
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
9671
+ try {
9672
+ const sessionId = await createOpenCodeSession(client, { title: `Optima startup readiness probe ${now().toISOString()}`, directory: worktree });
9673
+ if (await openCodeSessionExists(client, sessionId)) return { ok: true, method: "session_create_probe", sessionId, attempts: attempt };
9674
+ lastError = "readiness_probe_session_not_visible";
9675
+ } catch (error) {
9676
+ lastError = error.message || "opencode_not_ready";
9677
+ }
9678
+ if (attempt < maxAttempts && delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
9679
+ }
9680
+ return { ok: false, reason: lastError, attempts: maxAttempts };
9681
+ }
9629
9682
  function assertOpenCodePromptAccepted(result) {
9630
9683
  const status = Number(result?.status || result?.response?.status || result?.error?.status || 0);
9631
9684
  if (status >= 400 || result?.ok === false || result?.error) {
@@ -9663,13 +9716,13 @@ async function readOpenCodeJsonResponse(response, endpointName) {
9663
9716
  return { raw };
9664
9717
  }
9665
9718
  }
9666
- async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, fetchImpl = globalThis.fetch } = {}) {
9719
+ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
9667
9720
  if (typeof fetchImpl !== "function") throw new Error("OpenCode direct prompt delivery requires fetch.");
9668
9721
  const root = normalizeOpenCodeBaseUrl(baseUrl, "");
9669
9722
  if (!root) throw new Error("OpenCode direct prompt delivery requires a base URL.");
9670
9723
  const encodedSession = encodeURIComponent(sessionId);
9671
9724
  const attempts = [
9672
- {
9725
+ legacyOnly ? null : {
9673
9726
  name: "v2 prompt",
9674
9727
  url: `${root}/api/session/${encodedSession}/prompt`,
9675
9728
  body: { prompt: { text }, delivery: "queue", resume: true },
@@ -9688,7 +9741,7 @@ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent,
9688
9741
  return { ok: true, method: "http", endpoint: "/session/{sessionID}/prompt_async", status: response.status, data: data?.data || null, response: data };
9689
9742
  }
9690
9743
  }
9691
- ];
9744
+ ].filter(Boolean);
9692
9745
  let firstError = null;
9693
9746
  for (const attempt of attempts) {
9694
9747
  const response = await fetchImpl(attempt.url, {
@@ -9722,7 +9775,7 @@ async function callOpenCodePromptWithFallbacks(method, sessionId, flatPayload, s
9722
9775
  }
9723
9776
  throw firstError;
9724
9777
  }
9725
- async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false } = {}) {
9778
+ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false } = {}) {
9726
9779
  const directBaseUrl = opencodeBaseUrl || baseUrl;
9727
9780
  const parts = [{ type: "text", text }];
9728
9781
  const flatPayload = { directory, agent, parts };
@@ -9746,7 +9799,7 @@ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, direct
9746
9799
  firstError ??= error;
9747
9800
  }
9748
9801
  }
9749
- if (directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, fetchImpl });
9802
+ if (directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, fetchImpl, legacyOnly });
9750
9803
  if (firstError) throw firstError;
9751
9804
  throw new Error("OpenCode client does not expose session.prompt or session.promptAsync.");
9752
9805
  }
@@ -9781,18 +9834,20 @@ function openCodeMessageText(message) {
9781
9834
  for (const part of partList) parts.push(part?.text, part?.content);
9782
9835
  return parts.filter((value) => typeof value === "string").join("\n");
9783
9836
  }
9784
- async function verifyOpenCodeSessionEventDelivery(client, { sessionId, beforeMessages = null, expectedText = "", markers = [], attempts = 3, delayMs = 25 } = {}) {
9837
+ async function verifyOpenCodeSessionEventDelivery(client, { sessionId, beforeMessages = null, expectedText = "", markers = [], attempts = 8, delayMs = 250 } = {}) {
9785
9838
  let lastError = "message_verification_unavailable";
9786
9839
  for (let attempt = 0; attempt < Math.max(1, attempts); attempt += 1) {
9787
9840
  try {
9788
9841
  const afterMessages = await readOpenCodeSessionMessages(client, { sessionId, limit: 50 });
9789
- if (!afterMessages) return { ok: true, method: "verification_unavailable", skipped: true };
9842
+ if (!afterMessages) return { ok: false, reason: "message_verification_unavailable" };
9790
9843
  const beforeCount = Array.isArray(beforeMessages) ? beforeMessages.length : null;
9791
- if (beforeCount !== null && afterMessages.length > beforeCount) return { ok: true, method: "message_count", beforeCount, afterCount: afterMessages.length };
9844
+ const lastMessage = afterMessages.at(-1) || null;
9845
+ const lastMessageId = lastMessage?.id || lastMessage?.messageID || lastMessage?.messageId || null;
9846
+ if (beforeCount !== null && afterMessages.length > beforeCount) return { ok: true, method: "message_count", beforeCount, afterCount: afterMessages.length, lastMessageId };
9792
9847
  const textNeedles = [expectedText, ...markers].map((value) => String(value || "").trim()).filter(Boolean);
9793
9848
  const haystack = afterMessages.slice(-20).map(openCodeMessageText).join("\n");
9794
9849
  const matched = textNeedles.find((needle) => haystack.includes(needle));
9795
- if (matched) return { ok: true, method: "message_text", marker: matched, beforeCount, afterCount: afterMessages.length };
9850
+ if (matched) return { ok: true, method: "message_text", marker: matched, beforeCount, afterCount: afterMessages.length, lastMessageId };
9796
9851
  lastError = "message_not_visible";
9797
9852
  } catch (error) {
9798
9853
  lastError = error.message || "message_verification_failed";
@@ -9827,21 +9882,28 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
9827
9882
  const blocker2 = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: error.message, source: "delivery_admission_failed" });
9828
9883
  return { ok: false, action: "message_delivery_failed", reason: error.message, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker2 };
9829
9884
  }
9830
- if (admissionVerification) return { ok: true, verification: admissionVerification, fallback: false };
9831
9885
  let verification = await verifySessionEventDelivery(openCodeClient, { sessionId, beforeMessages, expectedText: text, markers: eventMarkers });
9832
- if (verification?.ok) return { ok: true, verification, fallback: false };
9886
+ if (verification?.ok) return { ok: true, verification, admissionVerification, fallback: false };
9887
+ if (verification?.reason === "message_verification_unavailable" && !admissionVerification) {
9888
+ return { ok: true, verification: { ok: true, method: "legacy_prompt_accepted", skipped: true }, fallback: false };
9889
+ }
9890
+ if (admissionVerification) {
9891
+ appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_admitted_but_invisible", taskId, sessionId, admission: admissionVerification, reason: verification?.reason || "message_not_visible" });
9892
+ }
9833
9893
  const canFallbackDirect = Boolean(opencodeBaseUrl);
9834
9894
  if (canFallbackDirect) {
9835
9895
  const retryBeforeMessages = await readOpenCodeSessionMessages(openCodeClient, { sessionId, limit: 50 }).catch(() => beforeMessages);
9836
- const retrySendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: true });
9896
+ const retrySendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: true, legacyOnly: Boolean(admissionVerification) });
9837
9897
  try {
9838
9898
  admissionVerification = openCodePromptAdmissionVerification(retrySendResult, sessionId);
9839
9899
  } catch (error) {
9840
9900
  verification = { ok: false, reason: error.message };
9841
9901
  }
9842
- if (admissionVerification) return { ok: true, verification: admissionVerification, fallback: true };
9843
9902
  verification = await verifySessionEventDelivery(openCodeClient, { sessionId, beforeMessages: retryBeforeMessages, expectedText: text, markers: eventMarkers });
9844
- if (verification?.ok) return { ok: true, verification, fallback: true };
9903
+ if (verification?.ok) return { ok: true, verification, admissionVerification, fallback: true };
9904
+ if (verification?.reason === "message_verification_unavailable" && !admissionVerification) {
9905
+ return { ok: true, verification: { ok: true, method: "legacy_prompt_accepted", skipped: true }, fallback: true };
9906
+ }
9845
9907
  }
9846
9908
  const reason = verification?.reason || "message_delivery_failed";
9847
9909
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason, fallbackAttempted: canFallbackDirect });
@@ -9887,11 +9949,12 @@ async function recoverClickUpPmSession({ openCodeClient, sendSessionEvent, click
9887
9949
  const replacementMetadata = clearClickUpPendingSessionMetadata(setClickUpSessionMetadata(metadataWithRouting, config.routing.metadataKey, replacementSessionId), config.routing.metadataKey);
9888
9950
  await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: replacementMetadata });
9889
9951
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_succeeded", taskId, staleSessionId, replacementSessionId });
9890
- return { ok: true, action: "sent_to_replacement_session", taskId, sessionId: replacementSessionId, staleSessionId, replacementAttempted: true, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath, deliveryVerification: replacementDelivery.verification, deliveryFallback: replacementDelivery.fallback };
9952
+ return { ok: true, action: "sent_to_replacement_session", taskId, sessionId: replacementSessionId, staleSessionId, replacementAttempted: true, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath, deliveryVerification: replacementDelivery.verification, deliveryAdmission: replacementDelivery.admissionVerification, deliveryFallback: replacementDelivery.fallback, deliveryAttempts: replacementDelivery.fallback ? 2 : 1 };
9891
9953
  }
9892
- function formatClickUpWebhookPrompt({ eventType, taskId, payload, branch = "", worktree = "", deliveryEvidencePath = "" }) {
9954
+ function formatClickUpWebhookPrompt({ eventType, taskId, payload, branch = "", worktree = "", deliveryEvidencePath = "", humanRoleContext = [] }) {
9893
9955
  const comment = clickUpCommentFromPayload(payload);
9894
9956
  const commentText = clickUpCommentText(comment).trim();
9957
+ const humanRoleLines = formatHumanRoleContext(humanRoleContext);
9895
9958
  return [
9896
9959
  "[ClickUp Webhook Event]",
9897
9960
  `Event: ${eventType}`,
@@ -9899,9 +9962,12 @@ function formatClickUpWebhookPrompt({ eventType, taskId, payload, branch = "", w
9899
9962
  branch ? `Branch: ${branch}` : null,
9900
9963
  worktree ? `Worktree: ${worktree}` : null,
9901
9964
  deliveryEvidencePath ? `Delivery evidence path: ${deliveryEvidencePath}` : null,
9965
+ humanRoleLines ? "Human role fallback registry (Optima-provided; non-blocking if repo docs/core/humans.md is missing):" : null,
9966
+ humanRoleLines || null,
9902
9967
  commentText ? `Comment: ${commentText}` : null,
9903
9968
  "",
9904
9969
  "Handle this ClickUp Product Manager event using the ClickUp-first workflow rules. Do not assume broad chat routing; this event passed Optima webhook gates.",
9970
+ "If the task worktree lacks docs/core/humans.md, use the Optima-provided human role fallback registry above instead of blocking solely on that missing file.",
9905
9971
  deliveryEvidencePath ? `Final merge-trackable evidence must be written under ${deliveryEvidencePath}; use .optima only as local mirror/staging.` : null
9906
9972
  ].filter((part) => part !== null).join("\n");
9907
9973
  }
@@ -10152,7 +10218,8 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
10152
10218
  evidence_path: `.optima/evidences/${branchSafeClickUpId(taskId)}/SUMMARY.md`
10153
10219
  };
10154
10220
  const metadataWithRouting = setClickUpTaskRoutingMetadata(existingMetadata, routingMetadata);
10155
- const prompt = formatClickUpWebhookPrompt({ eventType, taskId, payload, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath });
10221
+ const humanRoleContext = buildHumanRoleContext({ roleIds: config.routing?.humanRoleClickUpIds });
10222
+ const prompt = formatClickUpWebhookPrompt({ eventType, taskId, payload, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, humanRoleContext });
10156
10223
  if (!existingSessionId) {
10157
10224
  let pendingSessionId = getNestedMetadataValue(metadata, clickUpPendingSessionKey(config.routing.metadataKey)) || state.pendingSessions?.[taskId];
10158
10225
  if (!pendingSessionId) {
@@ -10178,7 +10245,7 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
10178
10245
  await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: nextMetadata });
10179
10246
  const { [taskId]: _completedPending, ...remainingPending } = stateToPersist.pendingSessions || {};
10180
10247
  stateToPersist = { ...stateToPersist, pendingSessions: remainingPending };
10181
- return finish({ ok: true, action: "created_session", taskId, sessionId: pendingSessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryFallback: delivery.fallback });
10248
+ return finish({ ok: true, action: "created_session", taskId, sessionId: pendingSessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryAdmission: delivery.admissionVerification, deliveryFallback: delivery.fallback, deliveryAttempts: delivery.fallback ? 2 : 1 });
10182
10249
  }
10183
10250
  const sessionId = String(existingSessionId);
10184
10251
  if (await sessionExists(openCodeClient, sessionId)) {
@@ -10188,7 +10255,7 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
10188
10255
  const recovery2 = await recoverClickUpPmSession({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, staleSessionId: sessionId, sessionTitle, taskRoute, metadataWithRouting, config, prompt, eventMarkers: [taskId, eventType], deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, eventKey, createSession, verifySessionEventDelivery });
10189
10256
  return finish(recovery2);
10190
10257
  }
10191
- return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryFallback: delivery.fallback });
10258
+ return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryAdmission: delivery.admissionVerification, deliveryFallback: delivery.fallback, deliveryAttempts: delivery.fallback ? 2 : 1 });
10192
10259
  }
10193
10260
  const at = now().toISOString();
10194
10261
  appendClickUpWebhookLocalLog(worktree, { type: "missing_session", taskId, sessionId, host, at });
@@ -10199,8 +10266,11 @@ async function routeClickUpWebhookEvent(options = {}) {
10199
10266
  const taskId = clickUpTaskIdFromPayload(options.payload || {});
10200
10267
  return withClickUpTaskRouteLock(taskId, () => routeClickUpWebhookEventUnlocked(options));
10201
10268
  }
10202
- async function reconcileClickUpStartup({ config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, saveState = null, now = () => /* @__PURE__ */ new Date(), limit = CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT } = {}) {
10269
+ 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 } = {}) {
10203
10270
  if (!config || !clickupClient?.listAssignedTasks) return { ok: true, skipped: true, reason: "clickup_task_listing_unavailable", assigned: 0, comments: 0 };
10271
+ const readiness = await waitForReadiness(openCodeClient, { worktree, now });
10272
+ 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: [] } };
10204
10274
  let authorizedUserId = "";
10205
10275
  if (clickupClient?.getAuthorizedUser) {
10206
10276
  try {
@@ -10217,7 +10287,7 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10217
10287
  };
10218
10288
  const lastWebhookMs = clickUpTimestampMs(mutableState.lastWebhookAt);
10219
10289
  const ignored = new Set((config.routing?.ignoredStatuses || CLICKUP_WEBHOOK_TERMINAL_STATUSES).map(normalizeClickUpStatus));
10220
- const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0 };
10290
+ const routed = { assigned: 0, comments: 0, ignored: 0, errors: 0, undelivered: 0, tasks: [] };
10221
10291
  clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_started", lastWebhookAt: state.lastWebhookAt || null });
10222
10292
  const listed = await clickupClient.listAssignedTasks({ assigneeId: config.routing.productManagerAssigneeId, limit });
10223
10293
  const tasks = clickUpTaskListItems(listed).slice(0, limit);
@@ -10246,18 +10316,44 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10246
10316
  sessionExists,
10247
10317
  createSession,
10248
10318
  sendSessionEvent,
10319
+ verifySessionEventDelivery,
10249
10320
  saveState: persistState,
10250
10321
  now
10251
10322
  });
10252
- if (result?.ok && result.action !== "ignored") {
10323
+ const routeSummary = {
10324
+ taskId,
10325
+ action: result?.action || "unknown",
10326
+ ok: result?.ok === true,
10327
+ sessionId: result?.sessionId || result?.replacementSessionId || null,
10328
+ branch: result?.branch || null,
10329
+ worktree: result?.worktree || null,
10330
+ verification: result?.deliveryVerification?.method || result?.deliveryVerification?.reason || null,
10331
+ delivered: hasActionableClickUpRoute(result),
10332
+ attempts: result?.deliveryAttempts || null,
10333
+ finalMessageCount: result?.deliveryVerification?.afterCount ?? null,
10334
+ finalMessageId: result?.deliveryVerification?.lastMessageId || null
10335
+ };
10336
+ routed.tasks.push(routeSummary);
10337
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_routed", ...routeSummary });
10338
+ if (routeSummary.delivered) {
10253
10339
  routed.assigned += 1;
10340
+ } else if (result?.ok && result.action === "ignored") {
10341
+ routed.ignored += 1;
10254
10342
  } else if (result?.ok === false) {
10255
10343
  routed.errors += 1;
10344
+ routed.undelivered += 1;
10345
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: result?.action || "error", sessionId: routeSummary.sessionId, reason: result.reason || "startup_reconciliation_route_failed" });
10256
10346
  if (!result.blockerTag) await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: result.reason || "startup_reconciliation_route_failed", source: "startup_reconciliation_task" });
10347
+ } else {
10348
+ routed.undelivered += 1;
10349
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: routeSummary.action, sessionId: routeSummary.sessionId, reason: "route_not_actionable" });
10257
10350
  }
10258
10351
  } catch (error) {
10259
10352
  routed.errors += 1;
10353
+ routed.undelivered += 1;
10354
+ routed.tasks.push({ taskId, action: "error", ok: false, sessionId: null, branch: null, worktree: null, verification: error.message, delivered: false });
10260
10355
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_failed", taskId, message: error.message });
10356
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: "error", sessionId: null, reason: "startup_reconciliation_task_failed" });
10261
10357
  await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "startup_reconciliation_task_failed", source: "startup_reconciliation_task" });
10262
10358
  }
10263
10359
  if (!lastWebhookMs || !clickupClient?.getTaskComments) continue;
@@ -10280,6 +10376,7 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10280
10376
  sessionExists,
10281
10377
  createSession,
10282
10378
  sendSessionEvent,
10379
+ verifySessionEventDelivery,
10283
10380
  saveState: persistState,
10284
10381
  now
10285
10382
  });
@@ -10291,8 +10388,12 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
10291
10388
  await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "startup_reconciliation_comments_failed", source: "startup_reconciliation_comments" });
10292
10389
  }
10293
10390
  }
10391
+ const emptyPromptSessions = routed.tasks.filter((task) => task.delivered === false && task.sessionId);
10392
+ if (emptyPromptSessions.length > 0) {
10393
+ appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_validation_failed", tasks: emptyPromptSessions });
10394
+ }
10294
10395
  clickUpWebhookLifecycleLog(worktree, { type: "startup_reconciliation_finished", ...routed });
10295
- return { ok: routed.errors === 0, ...routed };
10396
+ return { ok: routed.errors === 0 && routed.undelivered === 0, ...routed, validation: { undelivered: routed.undelivered, emptyPromptSessions } };
10296
10397
  }
10297
10398
  function clickUpWebhookExpectedPath(config) {
10298
10399
  try {
@@ -11410,6 +11511,19 @@ ${additionFragment}`;
11410
11511
  delete agentConfig.tools.optima_prompt_workflow;
11411
11512
  }
11412
11513
  }
11514
+ if (id === "workflow_product_manager") {
11515
+ const humanRoleContext = buildHumanRoleContext({ roleIds: options.clickUpWebhookValidation?.config?.routing?.humanRoleClickUpIds });
11516
+ const humanRoleLines = formatHumanRoleContext(humanRoleContext);
11517
+ if (humanRoleLines) {
11518
+ agentConfig.prompt = `${agentConfig.prompt}
11519
+
11520
+ ## Optima Human Role Fallback Registry
11521
+
11522
+ ${humanRoleLines}
11523
+
11524
+ Use this Optima-provided fallback when the current task worktree lacks docs/core/humans.md; missing repo-local humans.md is not a hard blocker when this context or configured ClickUp IDs are present.`;
11525
+ }
11526
+ }
11413
11527
  ourAgents[id] = agentConfig;
11414
11528
  if (repoCfg.features?.debug_dumps !== false) {
11415
11529
  const debugPath = path2.join(debugDir, `${id}.md`);
@@ -12078,7 +12192,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
12078
12192
  }
12079
12193
  };
12080
12194
  }
12081
- 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, 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 };
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 };
12082
12196
 
12083
12197
  // src/sanitize_cli.js
12084
12198
  var { migrateLegacyOptimaLayout: migrateLegacyOptimaLayout2 } = OptimaPlugin.__internals;
@@ -21,7 +21,7 @@
21
21
 
22
22
  - `product_manager` may answer, investigate, operate dashboards, and pre-estimate "a qué huele" small/medium/large plus rough story points; WPM owns delivery routing.
23
23
  - Supported delivery task types are `Tarea`, `Bug`, `Doc`, and `PoC`; ignore `Idea`, legacy `Backlog` alias, `Hito`, `Nota de reunión`, and `Respuesta del formulario` unless converted or linked to delivery work.
24
- - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and automation config.
24
+ - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if a task worktree lacks that file, use the Optima-provided human role fallback context and configured ClickUp IDs instead of blocking solely on the missing repo-local file. Use role identifiers in workflow text and automation config.
25
25
  - Status actions are deterministic: `backlog` ignore, `plan` plan plus `Story Points`, test strategy, `Definition`, and `CTO`/`PO` assignment, `in progress` execute, `validation` split Tech Lead and Validator/QA gates, `merge` parent approval state after `CTO`/`PO` move a validated parent task there, and `completed`/`Closed` ignore unless reopened.
26
26
  - Store ClickUp `agent_metadata` JSON with session IDs per agent/type/task/subtask; keep `Definition` as the plan contract and final Documentation as delivered behavior docs.
27
27
  - `workflow_product_manager` is registered only when explicit ClickUp webhook mode is configured and the local webhook subscription state is active/valid.
@@ -13,7 +13,7 @@
13
13
  - Routing comes from `docs/core/task_model.md`: `tiny` lightweight, `standard` bounded, `complex` decomposed; full mode supports all, mini mode refuses complex.
14
14
  - `product_manager` may answer/investigate/dashboard/pre-estimate "a qué huele" plus rough story points; development asks become routed ClickUp tasks.
15
15
  - ClickUp-first types: execute `Tarea`, `Bug`, `Doc`, `PoC`; ignore `Idea`, legacy `Backlog` alias, `Hito`, `Nota de reunión`, `Respuesta del formulario` unless converted/linked.
16
- - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and automation config.
16
+ - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if missing in a task worktree, use Optima-provided fallback role context/configured ClickUp IDs instead of blocking solely on the missing file.
17
17
  - ClickUp-first statuses: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, test strategy, and `CTO`/`PO` assignment, `in progress` execute, `validation` split Tech Lead + Validator/QA gates; Validator/QA may merge validated subtasks directly into the parent branch; validated parents stay in `validation` for `CTO`/`PO` approval; `merge` means `CTO`/`PO` approved the parent and Validator/QA may attempt parent merge into `dev`; `completed`/`Closed` ignore unless reopened.
18
18
  - Shared-worktree rule: one active `implementation` task at a time; isolated `investigation`/`spec` may run in parallel if non-conflicting.
19
19
  - Git rules: principal workspace stays on `dev`, never `main`; parent task pulls remote once at start; subtasks trust parent local branch; PoC branches stay `poc/<clickup-task-id>`; subtasks PR to parent branch, parents PR to `dev`, releases PR `dev` -> `main`; failed/conflicted subtask or parent merges return the affected item to `in progress` for the coding owner; no direct `main` pushes.
@@ -20,7 +20,7 @@
20
20
  - Human-readable task/evidence summaries, validation results, AC coverage, documentation impact, blockers, reopen history, status-transition rationale, and final handoffs must be posted to linked ClickUp task/subtask comments or fields.
21
21
  - Raw logs stay in evidence storage; ClickUp receives concise summaries, paths/links, or relevant excerpts only, never wholesale raw logs.
22
22
  - WPM owns ClickUp `Story Points` during `plan`, re-estimation on material plan changes, `agent_metadata` session JSON, `Definition` plan-contract linking, and parent approval routing after validation.
23
- - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and automation config.
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 automation config.
24
24
  - Subtask merge authority belongs to Validator/QA after successful subtask validation: subtask PRs target and merge into the parent branch/workspace without `CTO`/`PO` approval.
25
25
  - Parent merge authority is split: after Tech Lead and Validator/QA pass, WPM/Validator assigns `CTO` and `PO` while the parent task remains in `validation`; `CTO`/`PO` approve by moving the parent task to `merge`; Validator/QA then attempts the parent PR merge into `dev`.
26
26
  - If a subtask or parent merge conflicts or fails, Validator/QA returns the affected ClickUp task/subtask to `in progress` and routes it back to the coding owner.
@@ -14,7 +14,7 @@
14
14
  - Sync human-readable task/evidence summaries, validation results, AC coverage, documentation impact, blockers, reopen history, status-transition rationale, and final handoffs to linked ClickUp task/subtask comments or fields.
15
15
  - Keep raw logs in evidence storage; ClickUp receives concise summaries, paths/links, or relevant excerpts only, never wholesale raw logs.
16
16
  - WPM owns `Story Points` during `plan`, re-estimation on material plan changes, `agent_metadata`, `Definition` plan-contract linking, and parent approval routing after validation.
17
- - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and automation config.
17
+ - 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.
18
18
  - Validator/QA may merge validated subtask PRs into the parent branch/workspace without `CTO`/`PO` approval.
19
19
  - Parent merge authority requires `CTO`/`PO` approval: after Tech Lead + Validator/QA pass, assign both roles while the parent stays in `validation`; they approve by moving it to `merge`; Validator/QA then attempts the parent PR merge into `dev`.
20
20
  - Failed or conflicted subtask/parent merges return the affected ClickUp item to `in progress` for the coding owner.
@@ -11,7 +11,7 @@
11
11
  - Delivery task types: `Tarea`, `Bug`, `Doc`, `PoC`.
12
12
  - Ignored task types: `Idea`, legacy `Backlog` alias, `Hito`, `Nota de reunión`, `Respuesta del formulario` unless converted or linked to delivery work; `Idea` is non-delivery.
13
13
  - `product_manager` may pre-estimate "a qué huele" small/medium/large plus rough story points, but development requests must be converted to routed ClickUp work.
14
- - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and automation config.
14
+ - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if a task worktree lacks that file, use Optima fallback role context and configured ClickUp IDs instead of blocking solely on the missing repo-local file. Use role identifiers in workflow text and automation config.
15
15
  - Status-to-action mapping: `backlog` -> `ignore`, `plan` -> `plan`, `in progress` -> `execute`, `validation` -> `validate`, `merge` -> `merge`, `completed`/`Closed` -> `ignore` unless reopened. For parent tasks, `merge` means `CTO`/`PO` have approved by moving the task out of `validation`; for subtasks, Validator/QA may merge after successful validation without waiting for `merge` human approval.
16
16
  - Branch-safe type slugs are lowercase ASCII: `tarea`, `bug`, `doc`, `poc`.
17
17
 
@@ -6,7 +6,7 @@
6
6
  - Slices: `foundation`, `core`, `logic`, `ui`, `polish`, `qa`, `docs`.
7
7
  - ClickUp-first delivery types: `Tarea`, `Bug`, `Doc`, `PoC`; ignored types: `Idea`, legacy `Backlog` alias, `Hito`, `Nota de reunión`, `Respuesta del formulario` unless converted/linked.
8
8
  - Product Manager without workflow never develops; it may pre-estimate "a qué huele" small/medium/large plus rough story points and route development into ClickUp tasks.
9
- - Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and automation config.
9
+ - 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.
10
10
  - ClickUp-first actions: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, test strategy, and `CTO`/`PO` assignment, `in progress` execute, `validation` split Tech Lead + Validator/QA; validated subtasks may be merged by Validator/QA into the parent branch without human approval; validated parent tasks assign `CTO`/`PO` and wait for them to move the task to `merge`; `merge` lets Validator/QA attempt parent PR merge into `dev`; `completed`/`Closed` ignore unless reopened.
11
11
  - Routing: keep `tiny` to one slice and usually one specialist; keep `standard` bounded; decompose `complex` into slice-based subtasks.
12
12
  - `complex + implementation` normally uses `workflow_runner` in full mode.
@@ -18,7 +18,7 @@ We adhere to a strict test pyramid strategy to ensure 100% reliability.
18
18
  - **Planning Participation:** Validator/QA participates during `plan` for test strategy, required new coverage, Playwright needs, regression scope, and documentation-validation expectations.
19
19
  - **Regression:** A full regression suite must be run by the Developer before handing over for technical review.
20
20
  - **Split Validation:** Tech Lead reviews architecture, code, PR readiness, standards, and repo-skill use; Validator/QA verifies tests, Playwright flows, regression, required coverage, evidence, and final documentation freshness.
21
- - **Human Role Registry:** Resolve `CTO` and `PO` from `docs/core/humans.md`; use role identifiers in workflow text and automation config.
21
+ - **Human Role Registry:** Resolve `CTO` and `PO` from `docs/core/humans.md` when present; if a task worktree lacks that file, use Optima fallback role context and configured ClickUp IDs instead of blocking solely on the missing repo-local file. Use role identifiers in workflow text and automation config.
22
22
  - **Merge Execution Gate:** Validator/QA may merge validated subtask PRs into the parent branch/workspace without human approval. Parent PRs to `dev` require Tech Lead and Validator/QA pass plus `CTO`/`PO` approval by moving the parent task to `merge` before Validator/QA attempts the merge. Any conflicted or failed merge returns the affected task/subtask to `in progress` for the coding owner.
23
23
  - **Documentation Gate:** Validator/QA must fail validation when final documentation is missing or outdated; `Definition` is only the plan contract, not the delivered documentation.
24
24
  - **QA Output Contract:** QA handoffs should state the test strategy used, the results observed, the AC coverage achieved, any documentation impact, open risks, and the recommended next step.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defend-tech/opencode-optima",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+ssh://git@github.com/defend-tech/opencode-optima.git"