@defend-tech/opencode-optima 0.1.61 → 0.1.63

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
@@ -19,7 +19,7 @@
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
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
- - 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.
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.
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>`.
25
25
  - Clarifications, blockers, dependencies, and reviews go through PMA.
@@ -21,7 +21,7 @@
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
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
- - 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.
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.
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.
27
27
  - Read relevant docs/tasks fully when they govern the current work. Prefer targeted CodeMap navigation before broad source search.
@@ -15,7 +15,7 @@ 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 `CTO` plus `PO` or the next owner.
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
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.
20
20
  - Keep raw logs in evidence; ClickUp gets summaries, paths/links, or excerpts only.
21
21
 
@@ -33,18 +33,19 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
33
33
 
34
34
  - Register only when Optima opt-in ClickUp webhook mode is configured and active/valid.
35
35
  - Webhook wakeup requires signed `X-Signature` HMAC SHA-256 verification, duplicate suppression, PM assignment/non-terminal status checks, and comment mention gating for `@Defend Tech Product Manager`.
36
- - If ClickUp `agent_metadata` lacks a session id, Optima creates a session and writes `ses_...`; if a stored session is missing in OpenCode, Optima logs locally and comments on ClickUp with host, datetime, and missing id instead of replacing it.
36
+ - Webhook routing is successful only after the worktree is OpenChamber-visible, an OpenCode `workflow_product_manager` session exists in that exact worktree directory, and prompt delivery/admission is verified. Worktree visibility alone is a launch failure, not successful routing.
37
37
  - Delivery task types: `Tarea`, `Bug`, `Doc`, `PoC`. Ignore unless converted/linked to delivery: `Idea`, legacy `Backlog`, `Hito`, `Nota de reunión`, `Respuesta del formulario`. Treat `Backlog` task type as `Idea`, not `backlog` status.
38
38
  - Branch-safe slugs: `Tarea` -> `tarea`, `Bug` -> `bug`, `Doc` -> `doc`, `PoC` -> `poc`. Unknown task type: pause and ask PMA/PO for clarification.
39
39
 
40
40
  ## Status Actions
41
41
 
42
42
  - Human registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if missing in the task worktree, use the Optima-provided Human Role Fallback Registry and configured ClickUp IDs instead of blocking solely on the missing repo-local file.
43
+ - 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.
43
44
  - `backlog`: ignore until prioritized.
44
- - `plan`: clarify AC/SCR/test strategy with Validator/QA; decompose; create/update Definition; estimate Story Points; remove PM assignee first; assign `CTO` + `PO` or next owner; target zero PM-assigned tasks.
45
- - `in progress`: execute through the assigned delivery agent or workflow runner.
46
- - `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.
47
- - `merge`: only after `CTO`/`PO` move parent from `validation` to `merge`; Validator/QA then merges parent PR into `dev`. Conflicts or merge failures return the affected item to `in progress`.
45
+ - `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
+ - `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
+ - `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`.
48
49
  - `completed` / `Closed`: no execution unless explicitly reopened.
49
50
 
50
51
  ## Git, Worktree, PR
@@ -58,18 +59,18 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
58
59
  - Unrelated active tasks in `.optima/tasks/current.md` must not block planning; move to/create the correct task worktree instead.
59
60
  - Parent setup pulls remote once; after parent branch creation, subtasks can trust the parent local branch without continuous remote polling.
60
61
  - Branches: parent `<clickup-task-type>/<parent-task-id>`; subtask `<clickup-task-type>/<parent-task-id>-subtask-<subtask-id>`; pending planned subtasks `<clickup-task-type>/<parent-task-id>-pending-<title-slug>`; PoC always `poc/<clickup-task-id>` and remains there unless productized later.
61
- - PR targets/start points: subtask -> parent branch and starts from the parent branch; if parent branch/worktree is missing, bootstrap the parent from `dev`/`origin/dev` first; parent -> `dev` only after Tech Lead + Validator/QA pass and `CTO`/`PO` move to `merge`; release -> `dev` to `main` only after explicit approval.
62
+ - PR targets/start points: subtask -> parent branch and starts from the parent branch; if parent branch/worktree is missing, bootstrap the parent from `dev`/`origin/dev` first; parent -> `dev` only after Tech Lead + Validator/QA pass and a parent-validation human `Approved` comment triggers merge automation; release -> `dev` to `main` only after explicit approval.
62
63
  - Preserve user work and unrelated dirty files. Stop and ask if unexpected changes appear.
63
64
 
64
65
  ## Operating Style
65
66
 
66
67
  - Orchestrate; do not silently implement specialist work yourself.
67
68
  - 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.
68
- - 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 `CTO` plus `PO` or next owner.
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.
69
70
  - On pickup, rewrite the ClickUp task description with the complete current description of what must be done; do not rely on status comments.
70
71
  - At plan completion, rewrite the ClickUp task description again with the complete final plan/Definition, distinct from the plan comment.
71
72
  - Estimate Story Points during `plan`, write them to ClickUp `Story Points`, and re-estimate when material plan changes alter scope/risk.
72
- - Before assigning `CTO`/`PO` or any next owner, remove the PM assignee and verify no task remains assigned to Workflow/Product Manager unless explicitly re-queued.
73
+ - Before assigning any next owner, remove the PM assignee and verify no task remains assigned to Workflow/Product Manager unless explicitly re-queued; when the next owner is `CTO`/`PO`, verify the allowlist condition and comment evidence first.
73
74
  - Final handoffs must include Summary, Work Performed, AC Coverage, Documentation Impact, Open Risks, Recommended Next Step, verification results, and commit/PR status.
74
75
 
75
76
  <include:plugin:Agents_Common.md>
package/dist/index.js CHANGED
@@ -8621,13 +8621,6 @@ var activeClickUpWebhookLifecycleRegistry = /* @__PURE__ */ new Map();
8621
8621
  var CLICKUP_WEBHOOK_CLEANUP_TIMEOUT_MS = 8e3;
8622
8622
  var CLICKUP_WEBHOOK_SIGNAL_STATE = Symbol.for("opencode-optima.clickup-webhook.signal-state");
8623
8623
  var activeClickUpTaskRoutes = /* @__PURE__ */ new Map();
8624
- var objectIdentityMap = /* @__PURE__ */ new WeakMap();
8625
- var objectIdentitySequence = 0;
8626
- function objectIdentity(value) {
8627
- if (!value || typeof value !== "object" && typeof value !== "function") return String(value ?? "");
8628
- if (!objectIdentityMap.has(value)) objectIdentityMap.set(value, `obj_${++objectIdentitySequence}`);
8629
- return objectIdentityMap.get(value);
8630
- }
8631
8624
  function isRootDirectory(candidate) {
8632
8625
  const resolved = path5.resolve(candidate);
8633
8626
  return resolved === path5.parse(resolved).root;
@@ -8753,7 +8746,6 @@ function hasActionableClickUpRoute(result = {}) {
8753
8746
  if (!result.sessionId) return false;
8754
8747
  if (result.action === "message_delivery_failed" || result.action === "error") return false;
8755
8748
  if (result.deliveryVerification?.ok === false) return false;
8756
- if (result.deliveryVerification?.method === "prompt_admission") return false;
8757
8749
  return true;
8758
8750
  }
8759
8751
  function determineClickUpMergeAuthority({ isSubtask = false, clickupStatus = "", validationPassed = false, mergeFailed = false, finalApprovalRoles = CLICKUP_FINAL_APPROVER_ROLES, humansRegistry } = {}) {
@@ -8845,9 +8837,9 @@ var CLICKUP_REQUIRED_SUMMARY_SECTIONS = [
8845
8837
  ];
8846
8838
  var CLICKUP_RAW_LOG_SECTION_NAMES = /* @__PURE__ */ new Set(["Raw Logs", "Logs", "Full Logs", "Command Output", "Transcript"]);
8847
8839
  var CLICKUP_TRANSITIONS = /* @__PURE__ */ new Map([
8848
- ["plan->in progress", { status: "in progress", assignFinalApprovers: true, comment: "Plan complete; assigning CTO+PO for visibility and moving to implementation." }],
8840
+ ["plan->in progress", { status: "in progress", comment: "Plan complete; moving to implementation without generic CTO/PO assignment." }],
8849
8841
  ["in progress->validation", { status: "validation", comment: "Implementation complete; ready for validation." }],
8850
- ["validation->merge", { status: "merge", assignFinalApprovers: true, comment: "Validation passed; parent task is ready for CTO+PO merge approval." }],
8842
+ ["validation->merge", { status: "merge", assignFinalApprovers: true, parentOnlyFinalApproval: true, comment: "Parent validation passed with a functional preview URL; ready for CTO/PO approval flow." }],
8851
8843
  ["validation->in progress", { status: "in progress", comment: "Validation failed; returning to implementation." }],
8852
8844
  ["merge->completed", { status: "completed", comment: "Merge complete; closing delivery task." }],
8853
8845
  ["completed->in progress", { status: "in progress", comment: "Task reopened; returning to implementation." }],
@@ -9586,11 +9578,14 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9586
9578
  const key = `${from}->${to}`;
9587
9579
  const rule = CLICKUP_TRANSITIONS.get(key);
9588
9580
  if (!rule) return { ok: false, dryRun: true, message: `Transition not allowed: ${key}` };
9589
- const assignsFinalApprovers = rule.assignFinalApprovers === true;
9590
- const requiresPlanCompletionContract = from === "plan" && assignsFinalApprovers;
9581
+ const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && isSubtask);
9582
+ const requiresPlanCompletionContract = from === "plan" && to === "in progress";
9591
9583
  const definitionContent = compactMarkdownValue(definition);
9592
9584
  const description = compactMarkdownValue(planDescription);
9593
9585
  const validationErrors = [];
9586
+ if (rule.parentOnlyFinalApproval && isSubtask) {
9587
+ validationErrors.push("Subtasks must not use the parent final-approval transition; merge validated subtasks directly into the parent branch/workspace.");
9588
+ }
9594
9589
  if (assignsFinalApprovers && !isRealClickUpAssigneeId(productManagerAssignee) && requireProductManagerAssignee !== false) {
9595
9590
  validationErrors.push("productManagerAssignee must be the configured ClickUp PM assignee ID before assigning final approvers.");
9596
9591
  }
@@ -9599,10 +9594,10 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9599
9594
  if (!definitionContent) validationErrors.push("definition is required and must contain the complete plan/Definition content at plan completion.");
9600
9595
  }
9601
9596
  if (validationErrors.length > 0) return clickUpPayloadValidationError(validationErrors);
9602
- const finalApprovers = rule.assignFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
9597
+ const finalApprovers = assignsFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
9603
9598
  const explicitRemovals = (removeAssignees || []).filter(Boolean);
9604
9599
  const normalizedProductManagerAssignee = isRealClickUpAssigneeId(productManagerAssignee) ? String(productManagerAssignee).trim() : "";
9605
- const removalTargets = rule.assignFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
9600
+ const removalTargets = assignsFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
9606
9601
  const assigned = [...new Set([...assignees || [], ...finalApprovers].filter(Boolean))].filter((assignee) => !removalTargets.includes(assignee));
9607
9602
  const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
9608
9603
  const fields = authority ? { merge_authority: JSON.stringify(authority) } : {};
@@ -9617,7 +9612,7 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9617
9612
  assignment_delta: {
9618
9613
  add: assigned,
9619
9614
  remove: removalTargets,
9620
- objective: rule.assignFinalApprovers ? "zero_product_manager_assigned_tasks" : "preserve_existing_owner_policy"
9615
+ objective: assignsFinalApprovers ? "zero_product_manager_assigned_tasks" : "preserve_existing_owner_policy"
9621
9616
  },
9622
9617
  comment: rule.comment,
9623
9618
  description,
@@ -10075,11 +10070,17 @@ function createTestClickUpApiClient(config) {
10075
10070
  __metadata: metadata
10076
10071
  };
10077
10072
  }
10078
- function normalizeClickUpWebhookApiResponse(response = {}, fallbackConfig = null) {
10079
- const webhook = response?.webhook || response?.data || response || {};
10073
+ function clickUpWebhookRemoteInactiveReason(webhook = {}) {
10080
10074
  const healthStatus = String(webhook.health?.status || webhook.health_status || "").trim().toLowerCase();
10081
10075
  const status = String(webhook.status || webhook.state || "").trim().toLowerCase();
10082
- const active = webhook.active !== false && !["inactive", "disabled", "suspended", "failed", "failing", "error", "errored", "paused"].includes(status) && !["failed", "failing", "unhealthy", "error", "errored", "suspended", "paused"].includes(healthStatus);
10076
+ if (["suspended", "paused", "failed", "failing", "unhealthy", "error", "errored"].includes(healthStatus)) return `remote_health_${healthStatus}`;
10077
+ if (["inactive", "disabled", "suspended", "failed", "failing", "error", "errored", "paused"].includes(status)) return `remote_status_${status}`;
10078
+ if (webhook.active === false) return "remote_inactive";
10079
+ return "";
10080
+ }
10081
+ function normalizeClickUpWebhookApiResponse(response = {}, fallbackConfig = null) {
10082
+ const webhook = response?.webhook || response?.data || response || {};
10083
+ const active = !clickUpWebhookRemoteInactiveReason(webhook);
10083
10084
  return sanitizeClickUpWebhookState({
10084
10085
  active,
10085
10086
  webhookId: webhook.id || webhook.webhook_id || webhook.webhookId,
@@ -10105,20 +10106,44 @@ function clickUpWebhookLocationCompatible(webhook = {}, config = {}) {
10105
10106
  }
10106
10107
  return true;
10107
10108
  }
10109
+ function clickUpWebhookEventsCompatible(webhook = {}, config = {}) {
10110
+ if (!Array.isArray(webhook.events) || webhook.events.length === 0) return false;
10111
+ const actualEvents = new Set(webhook.events);
10112
+ return [...new Set(config?.webhook?.events || [])].every((event) => actualEvents.has(event));
10113
+ }
10114
+ function clickUpWebhookRemoteCompatibilityReason(webhook = {}, config = {}) {
10115
+ if (!clickUpWebhookLocationCompatible(webhook, config)) return "remote_location_mismatch";
10116
+ if (!clickUpWebhookEventsCompatible(webhook, config)) return "remote_events_mismatch";
10117
+ return "";
10118
+ }
10119
+ function isClickUpWebhookRemoteSelfHealableReason(reason = "") {
10120
+ const normalized = String(reason || "");
10121
+ return normalized === "remote_inactive" || normalized.startsWith("remote_health_") || normalized.startsWith("remote_status_");
10122
+ }
10108
10123
  async function findReusableClickUpWebhook(config, clickupClient = null, existingState = null) {
10109
10124
  if (!clickupClient?.listWebhooks) return null;
10110
10125
  const listed = await clickupClient.listWebhooks({ teamId: config.teamId });
10111
- let secretlessMatch = null;
10126
+ let incompatibleMatch = null;
10112
10127
  for (const webhook of clickUpWebhookListItems(listed)) {
10113
10128
  const remote = normalizeClickUpWebhookApiResponse(webhook, config);
10114
10129
  if (remote.publicUrl !== config.webhook.publicUrl) continue;
10115
- if (!clickUpWebhookLocationCompatible(webhook, config)) continue;
10130
+ const compatibilityReason = clickUpWebhookRemoteCompatibilityReason(webhook, config);
10131
+ if (compatibilityReason) {
10132
+ if (compatibilityReason === "remote_events_mismatch" && remote.webhookId && !incompatibleMatch) incompatibleMatch = { ...remote, active: false, reason: compatibilityReason };
10133
+ continue;
10134
+ }
10116
10135
  const existingSecret = existingState?.webhookId === remote.webhookId ? existingState.secret : "";
10117
10136
  const reusable = remote.secret ? remote : { ...remote, secret: existingSecret };
10118
10137
  if (isClickUpWebhookStateActive(reusable, config)) return reusable;
10119
- if (remote.webhookId && !secretlessMatch) secretlessMatch = { ...remote, active: false, reason: "remote_secret_unavailable" };
10138
+ if (remote.webhookId && !incompatibleMatch) {
10139
+ incompatibleMatch = {
10140
+ ...remote,
10141
+ active: false,
10142
+ reason: remote.active === false ? clickUpWebhookRemoteInactiveReason(webhook) || "remote_inactive" : "remote_secret_unavailable"
10143
+ };
10144
+ }
10120
10145
  }
10121
- return secretlessMatch;
10146
+ return incompatibleMatch;
10122
10147
  }
10123
10148
  async function validateClickUpWebhookState(state, config, clickupClient = null, { allowRemoteUnhealthyLocalRecovery = false } = {}) {
10124
10149
  if (!isClickUpWebhookStateActive(state, config)) return { valid: false, reason: "state_incomplete" };
@@ -10133,7 +10158,7 @@ async function validateClickUpWebhookState(state, config, clickupClient = null,
10133
10158
  }
10134
10159
  const localStateValidation = await validateClickUpWebhookState(state, config, null);
10135
10160
  if (allowRemoteUnhealthyLocalRecovery && localStateValidation.valid && remote.webhookId === state.webhookId) {
10136
- const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && [...new Set(config.webhook.events || [])].every((event) => new Set(remote.events || []).has(event));
10161
+ const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && !clickUpWebhookRemoteCompatibilityReason(match, config);
10137
10162
  if (remoteConfigMatches) {
10138
10163
  return {
10139
10164
  ...localStateValidation,
@@ -10158,12 +10183,18 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
10158
10183
  const { config } = validation;
10159
10184
  const existing = existingState ? sanitizeClickUpWebhookState(existingState, config) : readClickUpWebhookState(worktree, config);
10160
10185
  const existingValidation = await validateClickUpWebhookState(existing, config, clickupClient, { allowRemoteUnhealthyLocalRecovery: true });
10161
- if (existingValidation.valid) {
10186
+ const canReplaceUnhealthyExisting = existingValidation.mode === "local_state_remote_unhealthy" && existingValidation.remote?.webhookId && clickupClient?.deleteWebhook && clickupClient?.createWebhook;
10187
+ if (existingValidation.valid && !canReplaceUnhealthyExisting) {
10162
10188
  const state = writeClickUpWebhookState(worktree, { ...existingValidation.state, recentEventKeys: existing.recentEventKeys || [] }, config);
10163
10189
  clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: existingValidation.mode });
10164
10190
  return { active: true, valid: true, mode: existingValidation.mode, limitation: existingValidation.limitation, state };
10165
10191
  }
10166
- const reusableRemote = await findReusableClickUpWebhook(config, clickupClient, existing);
10192
+ const reusableRemote = canReplaceUnhealthyExisting ? {
10193
+ ...existingValidation.remote,
10194
+ active: false,
10195
+ secret: existingValidation.remote.secret || existing.secret,
10196
+ reason: clickUpWebhookRemoteInactiveReason(existingValidation.remote) || existingValidation.reason || "remote_unhealthy_self_heal"
10197
+ } : await findReusableClickUpWebhook(config, clickupClient, existing);
10167
10198
  if (reusableRemote) {
10168
10199
  if (isClickUpWebhookStateActive(reusableRemote, config)) {
10169
10200
  const state = writeClickUpWebhookState(worktree, { ...reusableRemote, recentEventKeys: existing.recentEventKeys || [] }, config);
@@ -10171,10 +10202,31 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
10171
10202
  clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: mode2 });
10172
10203
  return { active: true, valid: true, mode: mode2, state };
10173
10204
  }
10174
- clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_webhook_exists_without_secret" });
10175
- return { active: false, valid: false, reason: reusableRemote.reason || "remote_webhook_exists_without_secret", state: existing, remote: reusableRemote };
10205
+ if (reusableRemote.active === false && reusableRemote.reason !== "remote_secret_unavailable") {
10206
+ if (!isClickUpWebhookRemoteSelfHealableReason(reusableRemote.reason)) {
10207
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_incompatible" });
10208
+ return { active: false, valid: false, reason: reusableRemote.reason || "remote_incompatible", state: existing, remote: reusableRemote };
10209
+ }
10210
+ if (clickupClient?.deleteWebhook && clickupClient?.createWebhook) {
10211
+ const deleteResult = await deleteClickUpWebhookBestEffort({ webhookId: reusableRemote.webhookId, clickupClient, worktree, reason: reusableRemote.reason || "remote_unhealthy_self_heal" });
10212
+ if (deleteResult.ok) {
10213
+ if (existing.webhookId === reusableRemote.webhookId) markClickUpWebhookInactive(worktree, existing, config);
10214
+ } else {
10215
+ const reason = deleteResult.reason || "remote_unhealthy_delete_failed";
10216
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason });
10217
+ return { active: false, valid: false, reason, state: existing, remote: reusableRemote, deleteResult };
10218
+ }
10219
+ } else {
10220
+ const reason = clickupClient?.deleteWebhook ? "remote_unhealthy_create_unavailable" : "remote_unhealthy_delete_unavailable";
10221
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason });
10222
+ return { active: false, valid: false, reason, state: existing, remote: reusableRemote };
10223
+ }
10224
+ } else {
10225
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_webhook_exists_without_secret" });
10226
+ return { active: false, valid: false, reason: reusableRemote.reason || "remote_webhook_exists_without_secret", state: existing, remote: reusableRemote };
10227
+ }
10176
10228
  }
10177
- if (existing.webhookId && clickupClient?.deleteWebhook) {
10229
+ if (existing.webhookId && existing.webhookId !== reusableRemote?.webhookId && clickupClient?.deleteWebhook) {
10178
10230
  await deleteClickUpWebhookBestEffort({ webhookId: existing.webhookId, clickupClient, worktree, reason: existingValidation.reason || "startup_self_heal" });
10179
10231
  markClickUpWebhookInactive(worktree, existing, config);
10180
10232
  }
@@ -10502,12 +10554,16 @@ function clearClickUpPendingSessionMetadata(metadata, metadataKey) {
10502
10554
  delete cursor[parts[parts.length - 1]];
10503
10555
  return JSON.stringify(sortJsonValue(root), null, 2);
10504
10556
  }
10505
- async function openCodeSessionExists(client, sessionId) {
10557
+ function withOptionalDirectoryQuery(payload, directory) {
10558
+ if (!directory) return payload;
10559
+ return { ...payload, query: { ...payload.query || {}, directory } };
10560
+ }
10561
+ async function openCodeSessionExists(client, sessionId, { directory = "" } = {}) {
10506
10562
  const getAttempts = [
10507
- { path: { id: sessionId } },
10508
- { path: { sessionID: sessionId } },
10509
- { sessionID: sessionId },
10510
- { id: sessionId }
10563
+ withOptionalDirectoryQuery({ path: { id: sessionId } }, directory),
10564
+ withOptionalDirectoryQuery({ path: { sessionID: sessionId } }, directory),
10565
+ directory ? { sessionID: sessionId, directory } : { sessionID: sessionId },
10566
+ directory ? { id: sessionId, directory } : { id: sessionId }
10511
10567
  ];
10512
10568
  if (typeof client?.session?.get === "function") {
10513
10569
  for (const attempt of getAttempts) {
@@ -10519,10 +10575,10 @@ async function openCodeSessionExists(client, sessionId) {
10519
10575
  }
10520
10576
  }
10521
10577
  const messageAttempts = [
10522
- { path: { id: sessionId }, query: { limit: 1 } },
10523
- { path: { sessionID: sessionId }, query: { limit: 1 } },
10524
- { sessionID: sessionId, limit: 1 },
10525
- { id: sessionId, limit: 1 }
10578
+ { path: { id: sessionId }, query: { limit: 1, ...directory ? { directory } : {} } },
10579
+ { path: { sessionID: sessionId }, query: { limit: 1, ...directory ? { directory } : {} } },
10580
+ directory ? { sessionID: sessionId, directory, limit: 1 } : { sessionID: sessionId, limit: 1 },
10581
+ directory ? { id: sessionId, directory, limit: 1 } : { id: sessionId, limit: 1 }
10526
10582
  ];
10527
10583
  if (typeof client?.session?.messages === "function") {
10528
10584
  for (const attempt of messageAttempts) {
@@ -10926,6 +10982,17 @@ async function verifyOpenCodeSessionEventDelivery(client, { sessionId, directory
10926
10982
  }
10927
10983
  return { ok: false, reason: lastError };
10928
10984
  }
10985
+ async function postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason, source } = {}) {
10986
+ if (typeof clickupClient?.postTaskComment !== "function") return { ok: false, skipped: true, reason: "post_comment_unavailable" };
10987
+ try {
10988
+ await clickupClient.postTaskComment({ taskId, comment: `Optima launch failure: ${reason || "launch_failed"}. Worktree visibility alone is not successful routing; Optima requires an OpenCode session plus verified prompt admission/delivery.` });
10989
+ appendClickUpWebhookLocalLog(worktree, { type: "launch_failure_comment_posted", taskId, reason, source });
10990
+ return { ok: true };
10991
+ } catch (error) {
10992
+ appendClickUpWebhookLocalLog(worktree, { type: "launch_failure_comment_failed", taskId, reason, source, message: error.message });
10993
+ return { ok: false, error: error.message };
10994
+ }
10995
+ }
10929
10996
  async function applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason, source, tagName = CLICKUP_BLOCKER_TAG_NAME } = {}) {
10930
10997
  if (typeof clickupClient?.addTaskTag !== "function") {
10931
10998
  appendClickUpWebhookLocalLog(worktree, { type: "blocker_tag_unavailable", taskId, reason, source, tagName });
@@ -10955,7 +11022,17 @@ function openCodeBlockingPromptVerification(result, sessionId) {
10955
11022
  }
10956
11023
  async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
10957
11024
  const beforeMessages = await readOpenCodeSessionMessages(openCodeClient, { sessionId, directory, limit: 50 }).catch(() => null);
10958
- const sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, allowDirectFallback: directPrompt });
11025
+ let sendResult;
11026
+ try {
11027
+ sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, allowDirectFallback: directPrompt });
11028
+ } catch (error) {
11029
+ const reason2 = error.message || "message_delivery_failed";
11030
+ appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason: reason2, fallbackAttempted: false });
11031
+ if (!applyBlockerOnFailure) return { ok: false, action: "message_delivery_failed", reason: reason2, taskId, sessionId, fallbackAttempted: false };
11032
+ const blocker2 = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: reason2, source: "delivery_send_failed" });
11033
+ const launchFailureComment2 = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: reason2, source: "delivery_send_failed" });
11034
+ return { ok: false, action: "message_delivery_failed", reason: reason2, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker2, launchFailureComment: launchFailureComment2 };
11035
+ }
10959
11036
  let blockingPromptVerification = null;
10960
11037
  let admissionVerification = null;
10961
11038
  try {
@@ -10965,7 +11042,8 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
10965
11042
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason: error.message, fallbackAttempted: false });
10966
11043
  if (!applyBlockerOnFailure) return { ok: false, action: "message_delivery_failed", reason: error.message, taskId, sessionId, fallbackAttempted: false };
10967
11044
  const blocker2 = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: error.message, source: "delivery_admission_failed" });
10968
- return { ok: false, action: "message_delivery_failed", reason: error.message, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker2 };
11045
+ const launchFailureComment2 = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: error.message, source: "delivery_admission_failed" });
11046
+ return { ok: false, action: "message_delivery_failed", reason: error.message, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker2, launchFailureComment: launchFailureComment2 };
10969
11047
  }
10970
11048
  if (blockingPromptVerification) return { ok: true, verification: blockingPromptVerification, admissionVerification: null, fallback: false };
10971
11049
  if (admissionVerification && acceptPromptAdmission) {
@@ -10980,7 +11058,8 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
10980
11058
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason, fallbackAttempted: false, httpFallbackDisabled: Boolean(opencodeBaseUrl) });
10981
11059
  if (!applyBlockerOnFailure) return { ok: false, action: "message_delivery_failed", reason, taskId, sessionId, fallbackAttempted: false };
10982
11060
  const blocker = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason, source: "delivery_verification_failed" });
10983
- return { ok: false, action: "message_delivery_failed", reason, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker };
11061
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason, source: "delivery_verification_failed" });
11062
+ return { ok: false, action: "message_delivery_failed", reason, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker, launchFailureComment };
10984
11063
  }
10985
11064
  async function recoverClickUpPmSession({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, staleSessionId, sessionTitle, taskRoute, metadataWithRouting, config, prompt, eventMarkers = [], deliveryEvidencePath, evidencePath, eventKey, createSession, verifySessionEventDelivery } = {}) {
10986
11065
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_started", taskId, staleSessionId, worktree: taskRoute?.worktree });
@@ -10990,12 +11069,14 @@ async function recoverClickUpPmSession({ openCodeClient, sendSessionEvent, click
10990
11069
  } catch (error) {
10991
11070
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_create_failed", taskId, staleSessionId, message: error.message });
10992
11071
  const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "replacement_session_create_failed", source: "pm_session_recovery" });
10993
- return { ok: false, action: "message_delivery_failed", reason: "replacement_session_create_failed", taskId, sessionId: staleSessionId, replacementAttempted: true, blockerTag, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11072
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "replacement_session_create_failed", source: "pm_session_recovery" });
11073
+ return { ok: false, action: "message_delivery_failed", reason: "replacement_session_create_failed", taskId, sessionId: staleSessionId, replacementAttempted: true, blockerTag, launchFailureComment, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
10994
11074
  }
10995
11075
  if (!String(replacementSessionId || "").startsWith("ses_")) {
10996
11076
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_create_invalid", taskId, staleSessionId, replacementSessionId });
10997
11077
  const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "replacement_session_create_failed", source: "pm_session_recovery" });
10998
- return { ok: false, action: "message_delivery_failed", reason: "replacement_session_create_failed", taskId, sessionId: staleSessionId, replacementAttempted: true, blockerTag, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11078
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "replacement_session_create_failed", source: "pm_session_recovery" });
11079
+ return { ok: false, action: "message_delivery_failed", reason: "replacement_session_create_failed", taskId, sessionId: staleSessionId, replacementAttempted: true, blockerTag, launchFailureComment, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
10999
11080
  }
11000
11081
  const replacementDelivery = await deliverClickUpSessionEventWithVerification({
11001
11082
  openCodeClient,
@@ -11017,7 +11098,8 @@ async function recoverClickUpPmSession({ openCodeClient, sendSessionEvent, click
11017
11098
  if (!replacementDelivery.ok) {
11018
11099
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_delivery_failed", taskId, staleSessionId, replacementSessionId, reason: replacementDelivery.reason });
11019
11100
  const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: replacementDelivery.reason || "replacement_delivery_failed", source: "pm_session_recovery" });
11020
- return { ...replacementDelivery, sessionId: staleSessionId, replacementSessionId, replacementAttempted: true, blockerTag, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11101
+ const launchFailureComment = replacementDelivery.launchFailureComment || await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: replacementDelivery.reason || "replacement_delivery_failed", source: "pm_session_recovery" });
11102
+ return { ...replacementDelivery, sessionId: staleSessionId, replacementSessionId, replacementAttempted: true, blockerTag, launchFailureComment, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11021
11103
  }
11022
11104
  const replacementMetadata = clearClickUpPendingSessionMetadata(setClickUpSessionMetadata(metadataWithRouting, config.routing.metadataKey, replacementSessionId), config.routing.metadataKey);
11023
11105
  await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: replacementMetadata });
@@ -11301,8 +11383,26 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11301
11383
  if (!existingSessionId) {
11302
11384
  let pendingSessionId = getNestedMetadataValue(metadata, clickUpPendingSessionKey(config.routing.metadataKey)) || state.pendingSessions?.[taskId];
11303
11385
  if (!pendingSessionId) {
11304
- pendingSessionId = await createSession(openCodeClient, { title: sessionTitle, directory: taskRoute.worktree, agent: config.routing.targetAgent });
11305
- if (!String(pendingSessionId || "").startsWith("ses_")) return { ok: false, action: "error", reason: "session_create_failed", taskId };
11386
+ try {
11387
+ pendingSessionId = await createSession(openCodeClient, { title: sessionTitle, directory: taskRoute.worktree, agent: config.routing.targetAgent });
11388
+ } catch (error) {
11389
+ appendClickUpWebhookLocalLog(worktree, { type: "pm_session_create_failed", taskId, worktree: taskRoute.worktree, message: error.message });
11390
+ const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "session_create_failed", source: "route_create_session" });
11391
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "session_create_failed", source: "route_create_session" });
11392
+ return finish({ ok: false, action: "error", reason: "session_create_failed", taskId, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, blockerTag, launchFailureComment });
11393
+ }
11394
+ if (!String(pendingSessionId || "").startsWith("ses_")) {
11395
+ appendClickUpWebhookLocalLog(worktree, { type: "pm_session_create_invalid", taskId, worktree: taskRoute.worktree, sessionId: pendingSessionId || null });
11396
+ const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "session_create_failed", source: "route_create_session" });
11397
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "session_create_failed", source: "route_create_session" });
11398
+ return finish({ ok: false, action: "error", reason: "session_create_failed", taskId, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, blockerTag, launchFailureComment });
11399
+ }
11400
+ if (!await sessionExists(openCodeClient, pendingSessionId, { directory: taskRoute.worktree })) {
11401
+ appendClickUpWebhookLocalLog(worktree, { type: "pm_session_create_directory_unverified", taskId, worktree: taskRoute.worktree, sessionId: pendingSessionId });
11402
+ const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "session_directory_unverified", source: "route_create_session" });
11403
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "session_directory_unverified", source: "route_create_session" });
11404
+ return finish({ ok: false, action: "error", reason: "session_directory_unverified", taskId, sessionId: pendingSessionId, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, blockerTag, launchFailureComment });
11405
+ }
11306
11406
  if (saveState) {
11307
11407
  saveState({
11308
11408
  ...state,
@@ -11318,7 +11418,20 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11318
11418
  throw error;
11319
11419
  }
11320
11420
  const delivery = await deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId: pendingSessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl, directPrompt: config.opencode?.promptDelivery === "http", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery });
11321
- if (!delivery.ok) return finish({ ...delivery, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path });
11421
+ if (!delivery.ok) {
11422
+ const failedMetadata = clearClickUpPendingSessionMetadata(metadataWithRouting, config.routing.metadataKey);
11423
+ let pendingCleared = false;
11424
+ try {
11425
+ await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: failedMetadata });
11426
+ pendingCleared = true;
11427
+ } catch (error) {
11428
+ appendClickUpWebhookLocalLog(worktree, { type: "pending_session_clear_failed", taskId, sessionId: pendingSessionId, message: error.message });
11429
+ }
11430
+ const { [taskId]: _failedPending, ...remainingPending2 } = stateToPersist.pendingSessions || {};
11431
+ stateToPersist = { ...stateToPersist, pendingSessions: remainingPending2 };
11432
+ appendClickUpWebhookLocalLog(worktree, { type: "pending_session_delivery_failed", taskId, sessionId: pendingSessionId, reason: delivery.reason || "message_delivery_failed", pendingCleared });
11433
+ return finish({ ...delivery, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, pendingCleared });
11434
+ }
11322
11435
  const nextMetadata = clearClickUpPendingSessionMetadata(setClickUpSessionMetadata(pendingMetadata, config.routing.metadataKey, pendingSessionId), config.routing.metadataKey);
11323
11436
  await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: nextMetadata });
11324
11437
  const { [taskId]: _completedPending, ...remainingPending } = stateToPersist.pendingSessions || {};
@@ -11326,7 +11439,7 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11326
11439
  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 });
11327
11440
  }
11328
11441
  const sessionId = String(existingSessionId);
11329
- if (await sessionExists(openCodeClient, sessionId)) {
11442
+ if (await sessionExists(openCodeClient, sessionId, { directory: taskRoute.worktree })) {
11330
11443
  if (typeof clickupClient?.updateTaskMetadata === "function") await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: metadataWithRouting });
11331
11444
  const delivery = await deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl, directPrompt: config.opencode?.promptDelivery === "http", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery, applyBlockerOnFailure: false });
11332
11445
  if (!delivery.ok) {
@@ -11454,6 +11567,7 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11454
11567
  routed.undelivered += 1;
11455
11568
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: result?.action || "error", sessionId: routeSummary.sessionId, reason: result.reason || "startup_reconciliation_route_failed" });
11456
11569
  if (!result.blockerTag) await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: result.reason || "startup_reconciliation_route_failed", source: "startup_reconciliation_task" });
11570
+ if (!result.launchFailureComment) await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: result.reason || "startup_reconciliation_route_failed", source: "startup_reconciliation_task" });
11457
11571
  } else {
11458
11572
  routed.undelivered += 1;
11459
11573
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: routeSummary.action, sessionId: routeSummary.sessionId, reason: "route_not_actionable" });
@@ -11561,14 +11675,13 @@ function clickUpListenerKey(config) {
11561
11675
  }
11562
11676
  function clickUpListenerFingerprint({ config, state, worktree, clickupClient, openCodeClient } = {}) {
11563
11677
  return JSON.stringify({
11678
+ teamId: config?.teamId || "",
11564
11679
  publicUrl: config?.webhook?.publicUrl || "",
11565
11680
  path: clickUpWebhookExpectedPath(config),
11566
- worktree: path5.resolve(worktree || process.cwd()),
11681
+ location: config?.webhook?.location || {},
11567
11682
  webhookId: state?.webhookId || "",
11568
11683
  secret: state?.secret || "",
11569
- events: config?.webhook?.events || [],
11570
- clickupClient: objectIdentity(clickupClient),
11571
- openCodeClient: objectIdentity(openCodeClient)
11684
+ events: config?.webhook?.events || []
11572
11685
  });
11573
11686
  }
11574
11687
  function startClickUpWebhookListener({ config, state, worktree, clickupClient, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
@@ -8628,13 +8628,6 @@ var activeClickUpWebhookLifecycleRegistry = /* @__PURE__ */ new Map();
8628
8628
  var CLICKUP_WEBHOOK_CLEANUP_TIMEOUT_MS = 8e3;
8629
8629
  var CLICKUP_WEBHOOK_SIGNAL_STATE = Symbol.for("opencode-optima.clickup-webhook.signal-state");
8630
8630
  var activeClickUpTaskRoutes = /* @__PURE__ */ new Map();
8631
- var objectIdentityMap = /* @__PURE__ */ new WeakMap();
8632
- var objectIdentitySequence = 0;
8633
- function objectIdentity(value) {
8634
- if (!value || typeof value !== "object" && typeof value !== "function") return String(value ?? "");
8635
- if (!objectIdentityMap.has(value)) objectIdentityMap.set(value, `obj_${++objectIdentitySequence}`);
8636
- return objectIdentityMap.get(value);
8637
- }
8638
8631
  function isRootDirectory(candidate) {
8639
8632
  const resolved = path5.resolve(candidate);
8640
8633
  return resolved === path5.parse(resolved).root;
@@ -8760,7 +8753,6 @@ function hasActionableClickUpRoute(result = {}) {
8760
8753
  if (!result.sessionId) return false;
8761
8754
  if (result.action === "message_delivery_failed" || result.action === "error") return false;
8762
8755
  if (result.deliveryVerification?.ok === false) return false;
8763
- if (result.deliveryVerification?.method === "prompt_admission") return false;
8764
8756
  return true;
8765
8757
  }
8766
8758
  function determineClickUpMergeAuthority({ isSubtask = false, clickupStatus = "", validationPassed = false, mergeFailed = false, finalApprovalRoles = CLICKUP_FINAL_APPROVER_ROLES, humansRegistry } = {}) {
@@ -8852,9 +8844,9 @@ var CLICKUP_REQUIRED_SUMMARY_SECTIONS = [
8852
8844
  ];
8853
8845
  var CLICKUP_RAW_LOG_SECTION_NAMES = /* @__PURE__ */ new Set(["Raw Logs", "Logs", "Full Logs", "Command Output", "Transcript"]);
8854
8846
  var CLICKUP_TRANSITIONS = /* @__PURE__ */ new Map([
8855
- ["plan->in progress", { status: "in progress", assignFinalApprovers: true, comment: "Plan complete; assigning CTO+PO for visibility and moving to implementation." }],
8847
+ ["plan->in progress", { status: "in progress", comment: "Plan complete; moving to implementation without generic CTO/PO assignment." }],
8856
8848
  ["in progress->validation", { status: "validation", comment: "Implementation complete; ready for validation." }],
8857
- ["validation->merge", { status: "merge", assignFinalApprovers: true, comment: "Validation passed; parent task is ready for CTO+PO merge approval." }],
8849
+ ["validation->merge", { status: "merge", assignFinalApprovers: true, parentOnlyFinalApproval: true, comment: "Parent validation passed with a functional preview URL; ready for CTO/PO approval flow." }],
8858
8850
  ["validation->in progress", { status: "in progress", comment: "Validation failed; returning to implementation." }],
8859
8851
  ["merge->completed", { status: "completed", comment: "Merge complete; closing delivery task." }],
8860
8852
  ["completed->in progress", { status: "in progress", comment: "Task reopened; returning to implementation." }],
@@ -9593,11 +9585,14 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9593
9585
  const key = `${from}->${to}`;
9594
9586
  const rule = CLICKUP_TRANSITIONS.get(key);
9595
9587
  if (!rule) return { ok: false, dryRun: true, message: `Transition not allowed: ${key}` };
9596
- const assignsFinalApprovers = rule.assignFinalApprovers === true;
9597
- const requiresPlanCompletionContract = from === "plan" && assignsFinalApprovers;
9588
+ const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && isSubtask);
9589
+ const requiresPlanCompletionContract = from === "plan" && to === "in progress";
9598
9590
  const definitionContent = compactMarkdownValue(definition);
9599
9591
  const description = compactMarkdownValue(planDescription);
9600
9592
  const validationErrors = [];
9593
+ if (rule.parentOnlyFinalApproval && isSubtask) {
9594
+ validationErrors.push("Subtasks must not use the parent final-approval transition; merge validated subtasks directly into the parent branch/workspace.");
9595
+ }
9601
9596
  if (assignsFinalApprovers && !isRealClickUpAssigneeId(productManagerAssignee) && requireProductManagerAssignee !== false) {
9602
9597
  validationErrors.push("productManagerAssignee must be the configured ClickUp PM assignee ID before assigning final approvers.");
9603
9598
  }
@@ -9606,10 +9601,10 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9606
9601
  if (!definitionContent) validationErrors.push("definition is required and must contain the complete plan/Definition content at plan completion.");
9607
9602
  }
9608
9603
  if (validationErrors.length > 0) return clickUpPayloadValidationError(validationErrors);
9609
- const finalApprovers = rule.assignFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
9604
+ const finalApprovers = assignsFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
9610
9605
  const explicitRemovals = (removeAssignees || []).filter(Boolean);
9611
9606
  const normalizedProductManagerAssignee = isRealClickUpAssigneeId(productManagerAssignee) ? String(productManagerAssignee).trim() : "";
9612
- const removalTargets = rule.assignFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
9607
+ const removalTargets = assignsFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
9613
9608
  const assigned = [...new Set([...assignees || [], ...finalApprovers].filter(Boolean))].filter((assignee) => !removalTargets.includes(assignee));
9614
9609
  const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
9615
9610
  const fields = authority ? { merge_authority: JSON.stringify(authority) } : {};
@@ -9624,7 +9619,7 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9624
9619
  assignment_delta: {
9625
9620
  add: assigned,
9626
9621
  remove: removalTargets,
9627
- objective: rule.assignFinalApprovers ? "zero_product_manager_assigned_tasks" : "preserve_existing_owner_policy"
9622
+ objective: assignsFinalApprovers ? "zero_product_manager_assigned_tasks" : "preserve_existing_owner_policy"
9628
9623
  },
9629
9624
  comment: rule.comment,
9630
9625
  description,
@@ -10082,11 +10077,17 @@ function createTestClickUpApiClient(config) {
10082
10077
  __metadata: metadata
10083
10078
  };
10084
10079
  }
10085
- function normalizeClickUpWebhookApiResponse(response = {}, fallbackConfig = null) {
10086
- const webhook = response?.webhook || response?.data || response || {};
10080
+ function clickUpWebhookRemoteInactiveReason(webhook = {}) {
10087
10081
  const healthStatus = String(webhook.health?.status || webhook.health_status || "").trim().toLowerCase();
10088
10082
  const status = String(webhook.status || webhook.state || "").trim().toLowerCase();
10089
- const active = webhook.active !== false && !["inactive", "disabled", "suspended", "failed", "failing", "error", "errored", "paused"].includes(status) && !["failed", "failing", "unhealthy", "error", "errored", "suspended", "paused"].includes(healthStatus);
10083
+ if (["suspended", "paused", "failed", "failing", "unhealthy", "error", "errored"].includes(healthStatus)) return `remote_health_${healthStatus}`;
10084
+ if (["inactive", "disabled", "suspended", "failed", "failing", "error", "errored", "paused"].includes(status)) return `remote_status_${status}`;
10085
+ if (webhook.active === false) return "remote_inactive";
10086
+ return "";
10087
+ }
10088
+ function normalizeClickUpWebhookApiResponse(response = {}, fallbackConfig = null) {
10089
+ const webhook = response?.webhook || response?.data || response || {};
10090
+ const active = !clickUpWebhookRemoteInactiveReason(webhook);
10090
10091
  return sanitizeClickUpWebhookState({
10091
10092
  active,
10092
10093
  webhookId: webhook.id || webhook.webhook_id || webhook.webhookId,
@@ -10112,20 +10113,44 @@ function clickUpWebhookLocationCompatible(webhook = {}, config = {}) {
10112
10113
  }
10113
10114
  return true;
10114
10115
  }
10116
+ function clickUpWebhookEventsCompatible(webhook = {}, config = {}) {
10117
+ if (!Array.isArray(webhook.events) || webhook.events.length === 0) return false;
10118
+ const actualEvents = new Set(webhook.events);
10119
+ return [...new Set(config?.webhook?.events || [])].every((event) => actualEvents.has(event));
10120
+ }
10121
+ function clickUpWebhookRemoteCompatibilityReason(webhook = {}, config = {}) {
10122
+ if (!clickUpWebhookLocationCompatible(webhook, config)) return "remote_location_mismatch";
10123
+ if (!clickUpWebhookEventsCompatible(webhook, config)) return "remote_events_mismatch";
10124
+ return "";
10125
+ }
10126
+ function isClickUpWebhookRemoteSelfHealableReason(reason = "") {
10127
+ const normalized = String(reason || "");
10128
+ return normalized === "remote_inactive" || normalized.startsWith("remote_health_") || normalized.startsWith("remote_status_");
10129
+ }
10115
10130
  async function findReusableClickUpWebhook(config, clickupClient = null, existingState = null) {
10116
10131
  if (!clickupClient?.listWebhooks) return null;
10117
10132
  const listed = await clickupClient.listWebhooks({ teamId: config.teamId });
10118
- let secretlessMatch = null;
10133
+ let incompatibleMatch = null;
10119
10134
  for (const webhook of clickUpWebhookListItems(listed)) {
10120
10135
  const remote = normalizeClickUpWebhookApiResponse(webhook, config);
10121
10136
  if (remote.publicUrl !== config.webhook.publicUrl) continue;
10122
- if (!clickUpWebhookLocationCompatible(webhook, config)) continue;
10137
+ const compatibilityReason = clickUpWebhookRemoteCompatibilityReason(webhook, config);
10138
+ if (compatibilityReason) {
10139
+ if (compatibilityReason === "remote_events_mismatch" && remote.webhookId && !incompatibleMatch) incompatibleMatch = { ...remote, active: false, reason: compatibilityReason };
10140
+ continue;
10141
+ }
10123
10142
  const existingSecret = existingState?.webhookId === remote.webhookId ? existingState.secret : "";
10124
10143
  const reusable = remote.secret ? remote : { ...remote, secret: existingSecret };
10125
10144
  if (isClickUpWebhookStateActive(reusable, config)) return reusable;
10126
- if (remote.webhookId && !secretlessMatch) secretlessMatch = { ...remote, active: false, reason: "remote_secret_unavailable" };
10145
+ if (remote.webhookId && !incompatibleMatch) {
10146
+ incompatibleMatch = {
10147
+ ...remote,
10148
+ active: false,
10149
+ reason: remote.active === false ? clickUpWebhookRemoteInactiveReason(webhook) || "remote_inactive" : "remote_secret_unavailable"
10150
+ };
10151
+ }
10127
10152
  }
10128
- return secretlessMatch;
10153
+ return incompatibleMatch;
10129
10154
  }
10130
10155
  async function validateClickUpWebhookState(state, config, clickupClient = null, { allowRemoteUnhealthyLocalRecovery = false } = {}) {
10131
10156
  if (!isClickUpWebhookStateActive(state, config)) return { valid: false, reason: "state_incomplete" };
@@ -10140,7 +10165,7 @@ async function validateClickUpWebhookState(state, config, clickupClient = null,
10140
10165
  }
10141
10166
  const localStateValidation = await validateClickUpWebhookState(state, config, null);
10142
10167
  if (allowRemoteUnhealthyLocalRecovery && localStateValidation.valid && remote.webhookId === state.webhookId) {
10143
- const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && [...new Set(config.webhook.events || [])].every((event) => new Set(remote.events || []).has(event));
10168
+ const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && !clickUpWebhookRemoteCompatibilityReason(match, config);
10144
10169
  if (remoteConfigMatches) {
10145
10170
  return {
10146
10171
  ...localStateValidation,
@@ -10165,12 +10190,18 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
10165
10190
  const { config } = validation;
10166
10191
  const existing = existingState ? sanitizeClickUpWebhookState(existingState, config) : readClickUpWebhookState(worktree, config);
10167
10192
  const existingValidation = await validateClickUpWebhookState(existing, config, clickupClient, { allowRemoteUnhealthyLocalRecovery: true });
10168
- if (existingValidation.valid) {
10193
+ const canReplaceUnhealthyExisting = existingValidation.mode === "local_state_remote_unhealthy" && existingValidation.remote?.webhookId && clickupClient?.deleteWebhook && clickupClient?.createWebhook;
10194
+ if (existingValidation.valid && !canReplaceUnhealthyExisting) {
10169
10195
  const state = writeClickUpWebhookState(worktree, { ...existingValidation.state, recentEventKeys: existing.recentEventKeys || [] }, config);
10170
10196
  clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: existingValidation.mode });
10171
10197
  return { active: true, valid: true, mode: existingValidation.mode, limitation: existingValidation.limitation, state };
10172
10198
  }
10173
- const reusableRemote = await findReusableClickUpWebhook(config, clickupClient, existing);
10199
+ const reusableRemote = canReplaceUnhealthyExisting ? {
10200
+ ...existingValidation.remote,
10201
+ active: false,
10202
+ secret: existingValidation.remote.secret || existing.secret,
10203
+ reason: clickUpWebhookRemoteInactiveReason(existingValidation.remote) || existingValidation.reason || "remote_unhealthy_self_heal"
10204
+ } : await findReusableClickUpWebhook(config, clickupClient, existing);
10174
10205
  if (reusableRemote) {
10175
10206
  if (isClickUpWebhookStateActive(reusableRemote, config)) {
10176
10207
  const state = writeClickUpWebhookState(worktree, { ...reusableRemote, recentEventKeys: existing.recentEventKeys || [] }, config);
@@ -10178,10 +10209,31 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
10178
10209
  clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: mode2 });
10179
10210
  return { active: true, valid: true, mode: mode2, state };
10180
10211
  }
10181
- clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_webhook_exists_without_secret" });
10182
- return { active: false, valid: false, reason: reusableRemote.reason || "remote_webhook_exists_without_secret", state: existing, remote: reusableRemote };
10212
+ if (reusableRemote.active === false && reusableRemote.reason !== "remote_secret_unavailable") {
10213
+ if (!isClickUpWebhookRemoteSelfHealableReason(reusableRemote.reason)) {
10214
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_incompatible" });
10215
+ return { active: false, valid: false, reason: reusableRemote.reason || "remote_incompatible", state: existing, remote: reusableRemote };
10216
+ }
10217
+ if (clickupClient?.deleteWebhook && clickupClient?.createWebhook) {
10218
+ const deleteResult = await deleteClickUpWebhookBestEffort({ webhookId: reusableRemote.webhookId, clickupClient, worktree, reason: reusableRemote.reason || "remote_unhealthy_self_heal" });
10219
+ if (deleteResult.ok) {
10220
+ if (existing.webhookId === reusableRemote.webhookId) markClickUpWebhookInactive(worktree, existing, config);
10221
+ } else {
10222
+ const reason = deleteResult.reason || "remote_unhealthy_delete_failed";
10223
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason });
10224
+ return { active: false, valid: false, reason, state: existing, remote: reusableRemote, deleteResult };
10225
+ }
10226
+ } else {
10227
+ const reason = clickupClient?.deleteWebhook ? "remote_unhealthy_create_unavailable" : "remote_unhealthy_delete_unavailable";
10228
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason });
10229
+ return { active: false, valid: false, reason, state: existing, remote: reusableRemote };
10230
+ }
10231
+ } else {
10232
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_webhook_exists_without_secret" });
10233
+ return { active: false, valid: false, reason: reusableRemote.reason || "remote_webhook_exists_without_secret", state: existing, remote: reusableRemote };
10234
+ }
10183
10235
  }
10184
- if (existing.webhookId && clickupClient?.deleteWebhook) {
10236
+ if (existing.webhookId && existing.webhookId !== reusableRemote?.webhookId && clickupClient?.deleteWebhook) {
10185
10237
  await deleteClickUpWebhookBestEffort({ webhookId: existing.webhookId, clickupClient, worktree, reason: existingValidation.reason || "startup_self_heal" });
10186
10238
  markClickUpWebhookInactive(worktree, existing, config);
10187
10239
  }
@@ -10509,12 +10561,16 @@ function clearClickUpPendingSessionMetadata(metadata, metadataKey) {
10509
10561
  delete cursor[parts[parts.length - 1]];
10510
10562
  return JSON.stringify(sortJsonValue(root), null, 2);
10511
10563
  }
10512
- async function openCodeSessionExists(client, sessionId) {
10564
+ function withOptionalDirectoryQuery(payload, directory) {
10565
+ if (!directory) return payload;
10566
+ return { ...payload, query: { ...payload.query || {}, directory } };
10567
+ }
10568
+ async function openCodeSessionExists(client, sessionId, { directory = "" } = {}) {
10513
10569
  const getAttempts = [
10514
- { path: { id: sessionId } },
10515
- { path: { sessionID: sessionId } },
10516
- { sessionID: sessionId },
10517
- { id: sessionId }
10570
+ withOptionalDirectoryQuery({ path: { id: sessionId } }, directory),
10571
+ withOptionalDirectoryQuery({ path: { sessionID: sessionId } }, directory),
10572
+ directory ? { sessionID: sessionId, directory } : { sessionID: sessionId },
10573
+ directory ? { id: sessionId, directory } : { id: sessionId }
10518
10574
  ];
10519
10575
  if (typeof client?.session?.get === "function") {
10520
10576
  for (const attempt of getAttempts) {
@@ -10526,10 +10582,10 @@ async function openCodeSessionExists(client, sessionId) {
10526
10582
  }
10527
10583
  }
10528
10584
  const messageAttempts = [
10529
- { path: { id: sessionId }, query: { limit: 1 } },
10530
- { path: { sessionID: sessionId }, query: { limit: 1 } },
10531
- { sessionID: sessionId, limit: 1 },
10532
- { id: sessionId, limit: 1 }
10585
+ { path: { id: sessionId }, query: { limit: 1, ...directory ? { directory } : {} } },
10586
+ { path: { sessionID: sessionId }, query: { limit: 1, ...directory ? { directory } : {} } },
10587
+ directory ? { sessionID: sessionId, directory, limit: 1 } : { sessionID: sessionId, limit: 1 },
10588
+ directory ? { id: sessionId, directory, limit: 1 } : { id: sessionId, limit: 1 }
10533
10589
  ];
10534
10590
  if (typeof client?.session?.messages === "function") {
10535
10591
  for (const attempt of messageAttempts) {
@@ -10933,6 +10989,17 @@ async function verifyOpenCodeSessionEventDelivery(client, { sessionId, directory
10933
10989
  }
10934
10990
  return { ok: false, reason: lastError };
10935
10991
  }
10992
+ async function postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason, source } = {}) {
10993
+ if (typeof clickupClient?.postTaskComment !== "function") return { ok: false, skipped: true, reason: "post_comment_unavailable" };
10994
+ try {
10995
+ await clickupClient.postTaskComment({ taskId, comment: `Optima launch failure: ${reason || "launch_failed"}. Worktree visibility alone is not successful routing; Optima requires an OpenCode session plus verified prompt admission/delivery.` });
10996
+ appendClickUpWebhookLocalLog(worktree, { type: "launch_failure_comment_posted", taskId, reason, source });
10997
+ return { ok: true };
10998
+ } catch (error) {
10999
+ appendClickUpWebhookLocalLog(worktree, { type: "launch_failure_comment_failed", taskId, reason, source, message: error.message });
11000
+ return { ok: false, error: error.message };
11001
+ }
11002
+ }
10936
11003
  async function applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason, source, tagName = CLICKUP_BLOCKER_TAG_NAME } = {}) {
10937
11004
  if (typeof clickupClient?.addTaskTag !== "function") {
10938
11005
  appendClickUpWebhookLocalLog(worktree, { type: "blocker_tag_unavailable", taskId, reason, source, tagName });
@@ -10962,7 +11029,17 @@ function openCodeBlockingPromptVerification(result, sessionId) {
10962
11029
  }
10963
11030
  async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
10964
11031
  const beforeMessages = await readOpenCodeSessionMessages(openCodeClient, { sessionId, directory, limit: 50 }).catch(() => null);
10965
- const sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, allowDirectFallback: directPrompt });
11032
+ let sendResult;
11033
+ try {
11034
+ sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, allowDirectFallback: directPrompt });
11035
+ } catch (error) {
11036
+ const reason2 = error.message || "message_delivery_failed";
11037
+ appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason: reason2, fallbackAttempted: false });
11038
+ if (!applyBlockerOnFailure) return { ok: false, action: "message_delivery_failed", reason: reason2, taskId, sessionId, fallbackAttempted: false };
11039
+ const blocker2 = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: reason2, source: "delivery_send_failed" });
11040
+ const launchFailureComment2 = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: reason2, source: "delivery_send_failed" });
11041
+ return { ok: false, action: "message_delivery_failed", reason: reason2, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker2, launchFailureComment: launchFailureComment2 };
11042
+ }
10966
11043
  let blockingPromptVerification = null;
10967
11044
  let admissionVerification = null;
10968
11045
  try {
@@ -10972,7 +11049,8 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
10972
11049
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason: error.message, fallbackAttempted: false });
10973
11050
  if (!applyBlockerOnFailure) return { ok: false, action: "message_delivery_failed", reason: error.message, taskId, sessionId, fallbackAttempted: false };
10974
11051
  const blocker2 = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: error.message, source: "delivery_admission_failed" });
10975
- return { ok: false, action: "message_delivery_failed", reason: error.message, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker2 };
11052
+ const launchFailureComment2 = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: error.message, source: "delivery_admission_failed" });
11053
+ return { ok: false, action: "message_delivery_failed", reason: error.message, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker2, launchFailureComment: launchFailureComment2 };
10976
11054
  }
10977
11055
  if (blockingPromptVerification) return { ok: true, verification: blockingPromptVerification, admissionVerification: null, fallback: false };
10978
11056
  if (admissionVerification && acceptPromptAdmission) {
@@ -10987,7 +11065,8 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
10987
11065
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason, fallbackAttempted: false, httpFallbackDisabled: Boolean(opencodeBaseUrl) });
10988
11066
  if (!applyBlockerOnFailure) return { ok: false, action: "message_delivery_failed", reason, taskId, sessionId, fallbackAttempted: false };
10989
11067
  const blocker = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason, source: "delivery_verification_failed" });
10990
- return { ok: false, action: "message_delivery_failed", reason, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker };
11068
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason, source: "delivery_verification_failed" });
11069
+ return { ok: false, action: "message_delivery_failed", reason, taskId, sessionId, fallbackAttempted: false, blockerTag: blocker, launchFailureComment };
10991
11070
  }
10992
11071
  async function recoverClickUpPmSession({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, staleSessionId, sessionTitle, taskRoute, metadataWithRouting, config, prompt, eventMarkers = [], deliveryEvidencePath, evidencePath, eventKey, createSession, verifySessionEventDelivery } = {}) {
10993
11072
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_started", taskId, staleSessionId, worktree: taskRoute?.worktree });
@@ -10997,12 +11076,14 @@ async function recoverClickUpPmSession({ openCodeClient, sendSessionEvent, click
10997
11076
  } catch (error) {
10998
11077
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_create_failed", taskId, staleSessionId, message: error.message });
10999
11078
  const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "replacement_session_create_failed", source: "pm_session_recovery" });
11000
- return { ok: false, action: "message_delivery_failed", reason: "replacement_session_create_failed", taskId, sessionId: staleSessionId, replacementAttempted: true, blockerTag, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11079
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "replacement_session_create_failed", source: "pm_session_recovery" });
11080
+ return { ok: false, action: "message_delivery_failed", reason: "replacement_session_create_failed", taskId, sessionId: staleSessionId, replacementAttempted: true, blockerTag, launchFailureComment, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11001
11081
  }
11002
11082
  if (!String(replacementSessionId || "").startsWith("ses_")) {
11003
11083
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_create_invalid", taskId, staleSessionId, replacementSessionId });
11004
11084
  const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "replacement_session_create_failed", source: "pm_session_recovery" });
11005
- return { ok: false, action: "message_delivery_failed", reason: "replacement_session_create_failed", taskId, sessionId: staleSessionId, replacementAttempted: true, blockerTag, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11085
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "replacement_session_create_failed", source: "pm_session_recovery" });
11086
+ return { ok: false, action: "message_delivery_failed", reason: "replacement_session_create_failed", taskId, sessionId: staleSessionId, replacementAttempted: true, blockerTag, launchFailureComment, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11006
11087
  }
11007
11088
  const replacementDelivery = await deliverClickUpSessionEventWithVerification({
11008
11089
  openCodeClient,
@@ -11024,7 +11105,8 @@ async function recoverClickUpPmSession({ openCodeClient, sendSessionEvent, click
11024
11105
  if (!replacementDelivery.ok) {
11025
11106
  appendClickUpWebhookLocalLog(worktree, { type: "pm_session_recovery_delivery_failed", taskId, staleSessionId, replacementSessionId, reason: replacementDelivery.reason });
11026
11107
  const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: replacementDelivery.reason || "replacement_delivery_failed", source: "pm_session_recovery" });
11027
- return { ...replacementDelivery, sessionId: staleSessionId, replacementSessionId, replacementAttempted: true, blockerTag, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11108
+ const launchFailureComment = replacementDelivery.launchFailureComment || await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: replacementDelivery.reason || "replacement_delivery_failed", source: "pm_session_recovery" });
11109
+ return { ...replacementDelivery, sessionId: staleSessionId, replacementSessionId, replacementAttempted: true, blockerTag, launchFailureComment, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath };
11028
11110
  }
11029
11111
  const replacementMetadata = clearClickUpPendingSessionMetadata(setClickUpSessionMetadata(metadataWithRouting, config.routing.metadataKey, replacementSessionId), config.routing.metadataKey);
11030
11112
  await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: replacementMetadata });
@@ -11308,8 +11390,26 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11308
11390
  if (!existingSessionId) {
11309
11391
  let pendingSessionId = getNestedMetadataValue(metadata, clickUpPendingSessionKey(config.routing.metadataKey)) || state.pendingSessions?.[taskId];
11310
11392
  if (!pendingSessionId) {
11311
- pendingSessionId = await createSession(openCodeClient, { title: sessionTitle, directory: taskRoute.worktree, agent: config.routing.targetAgent });
11312
- if (!String(pendingSessionId || "").startsWith("ses_")) return { ok: false, action: "error", reason: "session_create_failed", taskId };
11393
+ try {
11394
+ pendingSessionId = await createSession(openCodeClient, { title: sessionTitle, directory: taskRoute.worktree, agent: config.routing.targetAgent });
11395
+ } catch (error) {
11396
+ appendClickUpWebhookLocalLog(worktree, { type: "pm_session_create_failed", taskId, worktree: taskRoute.worktree, message: error.message });
11397
+ const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "session_create_failed", source: "route_create_session" });
11398
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "session_create_failed", source: "route_create_session" });
11399
+ return finish({ ok: false, action: "error", reason: "session_create_failed", taskId, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, blockerTag, launchFailureComment });
11400
+ }
11401
+ if (!String(pendingSessionId || "").startsWith("ses_")) {
11402
+ appendClickUpWebhookLocalLog(worktree, { type: "pm_session_create_invalid", taskId, worktree: taskRoute.worktree, sessionId: pendingSessionId || null });
11403
+ const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "session_create_failed", source: "route_create_session" });
11404
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "session_create_failed", source: "route_create_session" });
11405
+ return finish({ ok: false, action: "error", reason: "session_create_failed", taskId, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, blockerTag, launchFailureComment });
11406
+ }
11407
+ if (!await sessionExists(openCodeClient, pendingSessionId, { directory: taskRoute.worktree })) {
11408
+ appendClickUpWebhookLocalLog(worktree, { type: "pm_session_create_directory_unverified", taskId, worktree: taskRoute.worktree, sessionId: pendingSessionId });
11409
+ const blockerTag = await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: "session_directory_unverified", source: "route_create_session" });
11410
+ const launchFailureComment = await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: "session_directory_unverified", source: "route_create_session" });
11411
+ return finish({ ok: false, action: "error", reason: "session_directory_unverified", taskId, sessionId: pendingSessionId, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, blockerTag, launchFailureComment });
11412
+ }
11313
11413
  if (saveState) {
11314
11414
  saveState({
11315
11415
  ...state,
@@ -11325,7 +11425,20 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11325
11425
  throw error;
11326
11426
  }
11327
11427
  const delivery = await deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId: pendingSessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl, directPrompt: config.opencode?.promptDelivery === "http", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery });
11328
- if (!delivery.ok) return finish({ ...delivery, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path });
11428
+ if (!delivery.ok) {
11429
+ const failedMetadata = clearClickUpPendingSessionMetadata(metadataWithRouting, config.routing.metadataKey);
11430
+ let pendingCleared = false;
11431
+ try {
11432
+ await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: failedMetadata });
11433
+ pendingCleared = true;
11434
+ } catch (error) {
11435
+ appendClickUpWebhookLocalLog(worktree, { type: "pending_session_clear_failed", taskId, sessionId: pendingSessionId, message: error.message });
11436
+ }
11437
+ const { [taskId]: _failedPending, ...remainingPending2 } = stateToPersist.pendingSessions || {};
11438
+ stateToPersist = { ...stateToPersist, pendingSessions: remainingPending2 };
11439
+ appendClickUpWebhookLocalLog(worktree, { type: "pending_session_delivery_failed", taskId, sessionId: pendingSessionId, reason: delivery.reason || "message_delivery_failed", pendingCleared });
11440
+ return finish({ ...delivery, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, pendingCleared });
11441
+ }
11329
11442
  const nextMetadata = clearClickUpPendingSessionMetadata(setClickUpSessionMetadata(pendingMetadata, config.routing.metadataKey, pendingSessionId), config.routing.metadataKey);
11330
11443
  await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: nextMetadata });
11331
11444
  const { [taskId]: _completedPending, ...remainingPending } = stateToPersist.pendingSessions || {};
@@ -11333,7 +11446,7 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11333
11446
  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 });
11334
11447
  }
11335
11448
  const sessionId = String(existingSessionId);
11336
- if (await sessionExists(openCodeClient, sessionId)) {
11449
+ if (await sessionExists(openCodeClient, sessionId, { directory: taskRoute.worktree })) {
11337
11450
  if (typeof clickupClient?.updateTaskMetadata === "function") await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: metadataWithRouting });
11338
11451
  const delivery = await deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl, directPrompt: config.opencode?.promptDelivery === "http", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery, applyBlockerOnFailure: false });
11339
11452
  if (!delivery.ok) {
@@ -11461,6 +11574,7 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11461
11574
  routed.undelivered += 1;
11462
11575
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: result?.action || "error", sessionId: routeSummary.sessionId, reason: result.reason || "startup_reconciliation_route_failed" });
11463
11576
  if (!result.blockerTag) await applyClickUpBlockerTag({ clickupClient, worktree, taskId, reason: result.reason || "startup_reconciliation_route_failed", source: "startup_reconciliation_task" });
11577
+ if (!result.launchFailureComment) await postClickUpLaunchFailureComment({ clickupClient, worktree, taskId, reason: result.reason || "startup_reconciliation_route_failed", source: "startup_reconciliation_task" });
11464
11578
  } else {
11465
11579
  routed.undelivered += 1;
11466
11580
  appendClickUpWebhookLocalLog(worktree, { type: "startup_reconciliation_task_undelivered", taskId, action: routeSummary.action, sessionId: routeSummary.sessionId, reason: "route_not_actionable" });
@@ -11568,14 +11682,13 @@ function clickUpListenerKey(config) {
11568
11682
  }
11569
11683
  function clickUpListenerFingerprint({ config, state, worktree, clickupClient, openCodeClient } = {}) {
11570
11684
  return JSON.stringify({
11685
+ teamId: config?.teamId || "",
11571
11686
  publicUrl: config?.webhook?.publicUrl || "",
11572
11687
  path: clickUpWebhookExpectedPath(config),
11573
- worktree: path5.resolve(worktree || process.cwd()),
11688
+ location: config?.webhook?.location || {},
11574
11689
  webhookId: state?.webhookId || "",
11575
11690
  secret: state?.secret || "",
11576
- events: config?.webhook?.events || [],
11577
- clickupClient: objectIdentity(clickupClient),
11578
- openCodeClient: objectIdentity(openCodeClient)
11691
+ events: config?.webhook?.events || []
11579
11692
  });
11580
11693
  }
11581
11694
  function startClickUpWebhookListener({ config, state, worktree, clickupClient, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
@@ -22,7 +22,8 @@
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
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
- - 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.
25
+ - Status actions are deterministic: `backlog` ignore, `plan` plan plus `Story Points`, test strategy, and `Definition`, `in progress` execute, `validation` split Tech Lead and Validator/QA gates, `merge` parent post-approval automation, and `completed`/`Closed` ignore unless reopened.
26
+ - Human approval assignment is prohibited except for the strict allowlist: parent `plan` with clear questions already posted in ClickUp comments; `in progress` blocked by missing credentials, permissions, external tools, or access; or parent `validation` with a functional preview URL such as `https://<taskid>-preview.defend.tech`. Do not assign `CTO`/`PO` for generic handoff, routine validation, cleanup, subtask planning/validation, or partial-phase stops.
26
27
  - 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
28
  - `workflow_product_manager` is registered only when explicit ClickUp webhook mode is configured and the local webhook subscription state is active/valid.
28
29
  - Webhook mode is opt-in: Optima validates signed `X-Signature` HMAC SHA-256 requests, routes status/assignee events only for Product Manager-assigned non-terminal tasks, routes comments only when they mention `@Defend Tech Product Manager`, stores new `ses_...` ids in ClickUp `agent_metadata`, and reports stale/missing sessions to ClickUp without creating replacements.
@@ -35,8 +36,8 @@
35
36
  - Parent task start pulls remote once; after branch creation, subtasks trust the parent local branch instead of continuous remote polling.
36
37
  - Parent branch format is `<clickup-task-type>/<parent-task-id>`; subtask branch format is the non-nested sibling ref `<clickup-task-type>/<parent-task-id>-subtask-<subtask-id>`; pending planned subtasks use `<clickup-task-type>/<parent-task-id>-pending-<title-slug>`; PoC branch format is always `poc/<clickup-task-id>` and stays there until a later productization task.
37
38
  - Subtask worktrees start from the parent branch and PR to the parent branch; if the parent branch/worktree is missing, bootstrap the parent from `dev`/`origin/dev` first. Parent task PRs target `dev`, and release PRs target `main` from `dev` only after explicit approval.
38
- - After successful subtask validation, Validator/QA merges the subtask PR into the parent branch/workspace without `CTO`/`PO` approval.
39
- - After parent Tech Lead and Validator/QA validation passes, the parent task stays in `validation`, `CTO`/`PO` are assigned and marked as approval owners, and they approve by moving it to `merge`; only then does Validator/QA attempt the parent PR merge into `dev`.
39
+ - After successful subtask validation, Validator/QA merges the subtask PR into the parent branch/workspace without `CTO`/`PO` assignment or approval.
40
+ - After parent Tech Lead and Validator/QA validation passes, the parent task may assign `CTO`/`PO` only when a functional validation URL is provided; after a human comments `Approved`, automation removes human assignees, assigns itself or the merge owner, merges the parent PR into `dev`, cleans workspaces/worktrees/branches, pushes to `dev`, and ensures the dev environment contains the code.
40
41
  - If any subtask or parent merge conflicts or fails, Validator/QA returns the affected ClickUp item to `in progress` and routes it to the coding owner.
41
42
  - Never push directly to `main`.
42
43
  - `investigation` and `spec` tasks may run in parallel only when they avoid conflicting delivery artifacts.
@@ -14,7 +14,7 @@
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
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
- - 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.
17
+ - ClickUp-first statuses: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, and test strategy; assign `CTO`/`PO` only for parent `plan` questions with clear ClickUp comments, real `in progress` blockers from missing credentials/tools/access, or parent `validation` with a functional preview URL. Subtasks merge directly into the parent branch after Validator/QA passes without CTO/PO assignment; parent `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.
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 branches use `<type>/<parent-id>`; subtask branches use non-nested `<type>/<parent-id>-subtask-<subtask-id>` and pending subtasks use `<type>/<parent-id>-pending-<title-slug>`; parent task pulls remote once at start; subtasks start from and PR to the parent local branch, bootstrapping the parent from `dev`/`origin/dev` first when missing; PoC branches stay `poc/<clickup-task-id>`; 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
20
  - Store `agent_metadata` session JSON; `Definition` is the plan contract, final Documentation is delivered behavior docs.
@@ -21,8 +21,9 @@
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
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
- - 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
- - 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`.
24
+ - Human approval assignment is prohibited except for three cases: parent `plan` with clear ClickUp-commented questions, `in progress` blockers caused by missing credentials/permissions/tools/access, or parent `validation` with a functional preview URL. Do not assign `CTO`/`PO` for generic handoff, routine validation, cleanup, subtask work, or partial-phase stops.
25
+ - Subtask merge authority belongs to Validator/QA after successful subtask validation: subtask PRs target and merge into the parent branch/workspace without `CTO`/`PO` assignment or approval.
26
+ - Parent merge authority is split: after Tech Lead and Validator/QA pass, WPM/Validator may assign `CTO`/`PO` only under the parent-validation allowlist; after a human comments `Approved`, automation removes human assignees, assigns itself or the merge owner, merges to `dev`, cleans workspaces/worktrees/branches, pushes to `dev`, and ensures the dev environment contains the code.
26
27
  - 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.
27
28
  - Git authority follows ClickUp-first rules: principal workspace on `dev`, no direct `main` push, parent task pulls remote once at start, subtask PRs to parent branch, parent PRs to `dev`, PoC branches stay `poc/<clickup-task-id>`, release PRs from `dev` to `main` only after approval.
28
29
 
@@ -15,8 +15,9 @@
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
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
- - Validator/QA may merge validated subtask PRs into the parent branch/workspace without `CTO`/`PO` approval.
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`.
18
+ - Human approval assignment is prohibited except for parent `plan` questions with clear ClickUp comments, `in progress` blockers from missing credentials/tools/access, or parent `validation` with a functional preview URL; never use it for generic handoff, cleanup, subtasks, or phase stops.
19
+ - Validator/QA may merge validated subtask PRs into the parent branch/workspace without `CTO`/`PO` assignment or approval.
20
+ - Parent merge authority uses the validation allowlist only: after a human comments `Approved`, automation removes human assignees, assigns merge owner/self, merges to `dev`, cleans workspaces/worktrees/branches, pushes, and ensures dev receives the code.
20
21
  - Failed or conflicted subtask/parent merges return the affected ClickUp item to `in progress` for the coding owner.
21
22
  - ClickUp-first Git rules: principal workspace on `dev`, no direct `main` push, parent pulls remote once at start, subtask PRs to parent branch, parent PRs to `dev`, PoC branches stay `poc/<clickup-task-id>`, release PRs `dev` -> `main` only after approval.
22
23
  - BA owns product truth and product-facing feature/domain docs.
@@ -12,9 +12,16 @@
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
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
- - 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.
15
+ - Status-to-action mapping: `backlog` -> `ignore`, `plan` -> `plan`, `in progress` -> `execute`, `validation` -> `validate`, `merge` -> `merge`, `completed`/`Closed` -> `ignore` unless reopened. `plan` does not imply generic human approval assignment; `merge` is parent-only post-approval automation, while subtasks merge after successful Validator/QA validation without waiting for a human approval status.
16
16
  - Branch-safe type slugs are lowercase ASCII: `tarea`, `bug`, `doc`, `poc`.
17
17
 
18
+ ## Human Approval Allowlist
19
+
20
+ - Agents must not assign ClickUp tasks to `CTO` or `PO` except for the explicit cases below; generic handoff, routine validation, duplicate-assignee cleanup, or incomplete phase handoff are prohibited.
21
+ - **Parent planning questions:** only a parent task in `plan` may assign `CTO`/`PO`, and only after clear, concrete questions have been posted in ClickUp comments. Subtasks are planned and executed end-to-end without CTO/PO planning assignment.
22
+ - **Real in-progress blocker:** a task in `in progress` may assign/escalate to `CTO`/`PO` only when blocked by missing credentials, permissions, external tools, or access. Agents must not stop at informal phase boundaries such as "I reached phase 1"; phases should have been subtasks, otherwise finish the accepted task.
23
+ - **Parent validation approval:** only a parent task in `validation` may assign `CTO`/`PO` for final validation, and only with a functional preview URL such as `https://<taskid>-preview.defend.tech` or an equivalent working validation URL. After a human comments `Approved`, automation reassigns to itself or the merge owner, removes human assignees, merges, cleans workspaces/worktrees/branches, pushes to `dev`, and ensures the dev environment contains the code.
24
+
18
25
  ## Routing Rules
19
26
 
20
27
  - `tiny` stays within one slice and usually one specialist handoff.
@@ -24,7 +31,7 @@
24
31
  - While one shared-worktree implementation task is active, parallel work is limited to non-conflicting `investigation` or `spec`.
25
32
  - WPM estimates `Story Points` during `plan`, re-estimates on material plan changes, links the `Definition` plan contract when needed, and records `agent_metadata` session IDs.
26
33
  - In ClickUp-first mode, work should be decomposed into parent/subtask branches: parent tasks pull remote once at start, parent branches use `<type>/<parent-id>` and merge to `dev`, subtasks use non-nested `<type>/<parent-id>-subtask-<subtask-id>` branches that start from/trust the parent local branch and merge to parent branches, missing parent branches/worktrees are bootstrapped from `dev`/`origin/dev` before subtask worktree creation, PoC branches stay `poc/<clickup-task-id>`, and release branches merge `dev` to `main` only after approval.
27
- - Validator/QA owns merge execution after the correct gate: validated subtask PRs merge directly into the parent branch/workspace, while parent PRs merge into `dev` only after `CTO`/`PO` move the parent task to `merge`. Merge conflicts or failed attempts return the affected task/subtask to `in progress` for the coding owner.
34
+ - Validator/QA owns merge execution after the correct gate: validated subtask PRs merge directly into the parent branch/workspace without CTO/PO assignment, while parent PRs merge into `dev` only after the parent validation approval allowlist is satisfied and a human `Approved` comment triggers merge automation. Merge conflicts or failed attempts return the affected task/subtask to `in progress` for the coding owner.
28
35
 
29
36
  ## Pre-Sync Defaults
30
37
 
@@ -7,7 +7,7 @@
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
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
- - 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.
10
+ - ClickUp-first actions: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, and test strategy; assign `CTO`/`PO` only for parent planning questions with clear ClickUp comments, real `in progress` blockers caused by missing credentials/tools/access, or parent `validation` with a functional preview URL. Subtasks execute end-to-end and merge to the parent branch after Validator/QA passes without CTO/PO approval; parent `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.
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.
13
13
  - WPM stores `agent_metadata`, re-estimates `Story Points` on material plan changes, and keeps `Definition` plan contract separate from final Documentation.
@@ -19,7 +19,8 @@ We adhere to a strict test pyramid strategy to ensure 100% reliability.
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
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
- - **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.
22
+ - **Human Approval Allowlist:** Assign `CTO`/`PO` only for parent `plan` questions with clear ClickUp comments, real `in progress` blockers caused by missing credentials/permissions/tools/access, or parent `validation` with a functional preview URL. Never assign them for generic handoff, routine validation, cleanup, subtasks, or partial-phase stops.
23
+ - **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 the parent-validation allowlist; after a human comments `Approved`, automation removes human assignees, assigns itself or the merge owner, merges, cleans workspaces/worktrees/branches, pushes to `dev`, and ensures the dev environment contains the code. Any conflicted or failed merge returns the affected task/subtask to `in progress` for the coding owner.
23
24
  - **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
25
  - **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.
25
26
  - **Investigation Outputs:** `investigation` tasks should produce findings, reproduction notes, logs when useful, and a recommended next step rather than pretending to be implementation tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defend-tech/opencode-optima",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+ssh://git@github.com/defend-tech/opencode-optima.git"