@defend-tech/opencode-optima 0.1.70 → 0.1.72

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.
@@ -45,8 +45,9 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
45
45
  - `backlog`: ignore until prioritized.
46
46
  - `plan`: clarify AC/SCR/test strategy with Validator/QA; decompose; create/update Definition; estimate Story Points; remove PM assignee first; assign the next delivery owner. Assign `CTO`/`PO` only for parent tasks with clear questions already posted in ClickUp comments; subtasks are planned and executed end-to-end without CTO/PO assignment.
47
47
  - `in progress`: execute through the assigned delivery agent or workflow runner. Treat blockers as work to solve first: spawn or resume Coder for code/build/dependency failures, QA for validation/test/evidence failures, Tech Lead for architecture/review/merge failures, and the relevant specialist for domain blockers. Escalate to `CTO`/`PO` only when genuinely blocked by missing credentials, permissions, external tools, or access after local/subagent resolution attempts; do not stop with phase language such as "I reached phase 1" or "no non-human assignee is available".
48
- - `validation`: route Tech Lead for architecture/code/PR/standards/repo-skill review and Validator/QA for tests, Playwright/regression/coverage/evidence/final-doc checks. Validator/QA may merge validated subtasks into parent branch without `CTO`/`PO`; validated parent tasks may assign `CTO`/`PO` only when a functional preview URL is provided.
49
- - `merge`: parent-only post-approval automation after a human comments `Approved`; remove human assignees, assign yourself or the merge owner, merge parent PR into `dev`, clean workspaces/worktrees/branches, push to `dev`, and ensure the dev environment contains the code. Conflicts or merge failures return the affected item to `in progress`.
48
+ - `validation`: before moving any task/subtask into Validation, create or update the required GitHub PR and store its URL/number in ClickUp `agent_metadata`. Subtasks PR from their subtask branch into the parent task branch. Parent tasks PR from the task branch into `dev`. A task in Validation without an open PR is invalid: move it back to `in progress`, open/update the PR, then return it to Validation. Route Tech Lead for architecture/code/PR/standards/repo-skill review and Validator/QA for tests, Playwright/regression/coverage/evidence/final-doc checks.
49
+ - GitHub review wakeups: keep listening for PR review/comment webhooks. Reply in GitHub to human comments. If a comment asks for or implies a change, reply first with what you will do, move the ClickUp task/subtask to `in progress`, delegate/implement the fix, push the same branch, update the PR, move ClickUp back to `validation`, then reply again in GitHub with what changed. Also add concise ClickUp status comments for model work state; never post Optima runtime/process noise.
50
+ - `merge`: parent-only post-approval automation after the configured final approver/CTO approves the GitHub PR. The GitHub accepted/approved review is the merge trigger. Merge the parent PR into `dev`, verify the Vercel preproduction deployment updates automatically, run a small smoke/regression against preproduction, and only then clean workspaces/worktrees/branches and move ClickUp to `completed`. If merge, Vercel deployment, or regression fails, create Bug subtasks under the parent task, move the parent back to `in progress`, and keep the evidence/PR links in ClickUp.
50
51
  - `completed` / `Closed`: no execution unless explicitly reopened.
51
52
 
52
53
  ## Git, Worktree, PR
@@ -60,7 +61,10 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
60
61
  - Unrelated active tasks in `.optima/tasks/current.md` must not block planning; move to/create the correct task worktree instead.
61
62
  - Parent setup pulls remote once; after parent branch creation, subtasks can trust the parent local branch without continuous remote polling.
62
63
  - 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.
63
- - 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.
64
+ - 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 task -> `dev` through a GitHub PR before entering Validation. Release -> `dev` to `main` only after explicit approval.
65
+ - Required PR gate: every transition into `validation` must have an open GitHub PR. Do not mark Validation with only local commits, evidence, or ClickUp comments.
66
+ - Review/merge cleanup: after the configured final approver/CTO approves and the PR is merged, keep the OpenCode session ids in ClickUp `agent_metadata`; delete only the merged branch/worktree after Vercel preproduction and smoke regression pass. If the task is reopened later, recreate/register the worktree from metadata/branch context and resume the preserved sessions.
67
+ - Final completion gate: parent task completion requires merged PR, Vercel preproduction deployment verified, smoke/regression evidence captured, no open review-change requests, final ClickUp comment with result, and status `completed`.
64
68
  - Preserve user work and unrelated dirty files. Stop and ask if unexpected changes appear.
65
69
 
66
70
  ## Operating Style
package/dist/index.js CHANGED
@@ -8819,6 +8819,70 @@ function deriveClickUpPrTarget({ isRelease = false, parentTaskId, parentBranch,
8819
8819
  }
8820
8820
  return devBranch;
8821
8821
  }
8822
+ function deriveClickUpRequiredPullRequest({ taskId, taskType = "Tarea", parentTaskId = "", subtaskId = "", branch = "", parentBranch = "", devBranch = "dev", mainBranch = "main", isRelease = false, githubRemote = "origin", prUrl = "", prNumber = "" } = {}) {
8823
+ const effectiveParent = parentTaskId || taskId;
8824
+ const isSubtask = isClickUpSubtaskRoute({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
8825
+ const sourceBranch = branch || deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
8826
+ const targetBranch = deriveClickUpPrTarget({
8827
+ isRelease,
8828
+ parentBranch: isSubtask ? parentBranch || deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent }) : parentBranch,
8829
+ parentTaskId: isSubtask ? effectiveParent : "",
8830
+ parentTaskType: taskType,
8831
+ taskType,
8832
+ devBranch,
8833
+ mainBranch
8834
+ });
8835
+ return {
8836
+ required: true,
8837
+ sourceBranch,
8838
+ targetBranch,
8839
+ githubRemote,
8840
+ prUrl: String(prUrl || "").trim(),
8841
+ prNumber: String(prNumber || "").trim(),
8842
+ scope: isSubtask ? "subtask_to_parent" : isRelease ? "release_to_main" : "task_to_dev",
8843
+ policy: "validation_requires_open_pull_request"
8844
+ };
8845
+ }
8846
+ function buildClickUpReviewAutomationPlan({ isSubtask = false, humanApprover = "CTO", preproductionProvider = "Vercel", preproductionEnvironment = "preproduction" } = {}) {
8847
+ return {
8848
+ github_webhooks: {
8849
+ listen_for: ["pull_request_review", "pull_request_review_comment", "issue_comment", "pull_request"],
8850
+ comment_policy: "reply_before_change_and_after_change",
8851
+ change_request_status: "in progress",
8852
+ after_fix_status: "validation",
8853
+ state_storage: "ClickUp agent_metadata.github"
8854
+ },
8855
+ review_comments: [
8856
+ "Answer GitHub comments directly in GitHub.",
8857
+ "If a comment implies a code/doc/test change, first reply with the intended action, move the ClickUp task/subtask to in progress, implement, push the same branch, move it back to validation, update the PR, then reply again with what changed.",
8858
+ "Also update ClickUp with human-readable model work status; runtime/process noise stays in local logs."
8859
+ ],
8860
+ accepted_review: {
8861
+ accepted_by: humanApprover,
8862
+ trigger: "approved_pull_request_review",
8863
+ action: isSubtask ? "merge_subtask_pr_into_parent_branch" : "merge_parent_pr_into_dev",
8864
+ delete_source_branch_after_success: true,
8865
+ delete_worktree_after_success: true,
8866
+ preserve_opencode_sessions_in_agent_metadata: true
8867
+ },
8868
+ preproduction: isSubtask ? {
8869
+ required: false,
8870
+ reason: "Subtask merge stops at parent branch; parent task owns dev/preproduction."
8871
+ } : {
8872
+ required: true,
8873
+ provider: preproductionProvider,
8874
+ environment: preproductionEnvironment,
8875
+ verify_deployment_after_merge: true,
8876
+ run_smoke_regression: true,
8877
+ on_failure: "create_bug_subtasks_and_return_parent_to_in_progress",
8878
+ completion_gate: "preproduction_deployed_and_regression_passed"
8879
+ },
8880
+ completion: {
8881
+ final_clickup_comment_required: true,
8882
+ final_status: isSubtask ? "completed_or_parent_validation" : "completed"
8883
+ }
8884
+ };
8885
+ }
8822
8886
  function validateMainWorkspaceBranchSafety({ currentBranch, requiredBranch = "dev", forbiddenBranch = "main" } = {}) {
8823
8887
  const normalized = String(currentBranch ?? "").trim();
8824
8888
  if (!normalized) {
@@ -9481,6 +9545,7 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9481
9545
  const worktree = deriveClickUpWorktree({ baseWorktree, taskId, taskType, parentTaskId: effectiveParent, subtaskId });
9482
9546
  const prTarget = isSubtask ? parentBranch : "dev";
9483
9547
  const startFrom = isSubtask ? parentBranch : "dev";
9548
+ const requiredPullRequest = deriveClickUpRequiredPullRequest({ taskId, taskType, parentTaskId: effectiveParent, subtaskId, branch, parentBranch });
9484
9549
  const estimate = preEstimateClickUpWork({ complexity, filesChanged, acceptanceCriteria, unknowns });
9485
9550
  const metadata = mergeClickUpAgentMetadata(existingAgentMetadata, {
9486
9551
  task: {
@@ -9494,6 +9559,10 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9494
9559
  mirror_task_path: `.optima/tasks/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}.md`,
9495
9560
  evidence_path: `.optima/evidences/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}/SUMMARY.md`,
9496
9561
  delivery_evidence_path: deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent),
9562
+ github: {
9563
+ required_pull_request: requiredPullRequest,
9564
+ review_automation: buildClickUpReviewAutomationPlan({ isSubtask })
9565
+ },
9497
9566
  ...agentMetadata
9498
9567
  }
9499
9568
  });
@@ -9516,6 +9585,10 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9516
9585
  parentBranch: parentBranch || void 0,
9517
9586
  prTarget
9518
9587
  },
9588
+ github: {
9589
+ requiredPullRequest,
9590
+ reviewAutomation: buildClickUpReviewAutomationPlan({ isSubtask })
9591
+ },
9519
9592
  mirror: {
9520
9593
  taskPath: `.optima/tasks/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}.md`,
9521
9594
  evidenceSummaryPath: `.optima/evidences/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}/SUMMARY.md`,
@@ -9526,14 +9599,15 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9526
9599
  wouldCreate: true
9527
9600
  },
9528
9601
  clickup: {
9529
- comment: `Starting task from ${startFrom}. Branch: ${branch}. Worktree: ${worktree}. Delivery evidence: ${deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent)}.`,
9602
+ comment: `Starting task from ${startFrom}. Branch: ${branch}. Worktree: ${worktree}. Validation PR target: ${requiredPullRequest.targetBranch}. Delivery evidence: ${deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent)}.`,
9530
9603
  description,
9531
9604
  fields: {
9532
9605
  Definition: definitionContent,
9533
9606
  "Story Points": estimate.storyPoints,
9534
9607
  agent_metadata: metadata,
9535
9608
  branch,
9536
- worktree
9609
+ worktree,
9610
+ pr_target: requiredPullRequest.targetBranch
9537
9611
  },
9538
9612
  definition_doc: {
9539
9613
  parent: normalizeClickUpDefinitionDocParent(definitionDocParent),
@@ -9543,23 +9617,29 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9543
9617
  }
9544
9618
  };
9545
9619
  }
9546
- function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed = false, validationFailed = false, isSubtask = false, reopen = false, assignees = [], removeAssignees = [], productManagerAssignee = "", requireProductManagerAssignee = true, planDescription = "", definition = "", definitionDocParent = CLICKUP_DEFINITION_DOC_PARENT, humansRegistry } = {}) {
9620
+ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed = false, validationFailed = false, isSubtask = false, reopen = false, assignees = [], removeAssignees = [], productManagerAssignee = "", requireProductManagerAssignee = true, planDescription = "", definition = "", definitionDocParent = CLICKUP_DEFINITION_DOC_PARENT, humansRegistry, taskId = "", taskType = "Tarea", parentTaskId = "", subtaskId = "", branch = "", parentBranch = "", prUrl = "", prNumber = "", githubRemote = "origin", devBranch = "dev", mainBranch = "main", preproductionProvider = "Vercel", preproductionEnvironment = "preproduction" } = {}) {
9547
9621
  const from = normalizeClickUpStatus(fromStatus);
9622
+ const effectiveIsSubtask = Boolean(isSubtask || isClickUpSubtaskRoute({ taskType, parentTaskId: parentTaskId || taskId, subtaskId, taskId }));
9548
9623
  let to = normalizeClickUpStatus(toStatus);
9549
9624
  if (validationFailed) to = "in progress";
9550
- if (!to && validationPassed) to = isSubtask ? "completed" : "merge";
9625
+ if (!to && validationPassed) to = effectiveIsSubtask ? "completed" : "merge";
9551
9626
  if ((from === "completed" || from === "closed") && !reopen) {
9552
9627
  return { ok: true, noop: true, dryRun: true, message: "Completed/closed task is a no-op unless reopen is explicit." };
9553
9628
  }
9554
9629
  const key = `${from}->${to}`;
9555
9630
  const rule = CLICKUP_TRANSITIONS.get(key);
9556
9631
  if (!rule) return { ok: false, dryRun: true, message: `Transition not allowed: ${key}` };
9557
- const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && isSubtask);
9632
+ const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && effectiveIsSubtask);
9558
9633
  const requiresPlanCompletionContract = from === "plan" && to === "in progress";
9634
+ const requiresValidationPullRequest = from === "in progress" && to === "validation";
9559
9635
  const definitionContent = compactMarkdownValue(definition);
9560
9636
  const description = compactMarkdownValue(planDescription);
9637
+ const canDerivePullRequest = Boolean(taskId || branch);
9638
+ const shouldAttachPullRequest = requiresValidationPullRequest || (from === "validation" || to === "merge" || from === "merge") && canDerivePullRequest;
9639
+ const requiredPullRequest = shouldAttachPullRequest && canDerivePullRequest ? deriveClickUpRequiredPullRequest({ taskId, taskType, parentTaskId, subtaskId, branch, parentBranch, devBranch, mainBranch, githubRemote, prUrl, prNumber }) : null;
9640
+ const reviewAutomation = requiredPullRequest ? buildClickUpReviewAutomationPlan({ isSubtask: effectiveIsSubtask, preproductionProvider, preproductionEnvironment }) : null;
9561
9641
  const validationErrors = [];
9562
- if (rule.parentOnlyFinalApproval && isSubtask) {
9642
+ if (rule.parentOnlyFinalApproval && effectiveIsSubtask) {
9563
9643
  validationErrors.push("Subtasks must not use the parent final-approval transition; merge validated subtasks directly into the parent branch/workspace.");
9564
9644
  }
9565
9645
  if (assignsFinalApprovers && !isRealClickUpAssigneeId(productManagerAssignee) && requireProductManagerAssignee !== false) {
@@ -9569,19 +9649,34 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9569
9649
  if (!description) validationErrors.push("planDescription is required for the plan-completion ClickUp task description rewrite.");
9570
9650
  if (!definitionContent) validationErrors.push("definition is required and must contain the complete plan/Definition content at plan completion.");
9571
9651
  }
9652
+ if (requiresValidationPullRequest) {
9653
+ if (!requiredPullRequest) validationErrors.push("taskId or branch is required before moving a ClickUp task to validation.");
9654
+ if (!requiredPullRequest?.sourceBranch) validationErrors.push("branch/sourceBranch is required before moving a ClickUp task to validation.");
9655
+ if (!requiredPullRequest?.targetBranch) validationErrors.push("PR target branch is required before moving a ClickUp task to validation.");
9656
+ if (!requiredPullRequest?.prUrl && !requiredPullRequest?.prNumber) validationErrors.push("An open GitHub pull request URL or number is required before moving a ClickUp task to validation.");
9657
+ }
9572
9658
  if (validationErrors.length > 0) return clickUpPayloadValidationError(validationErrors);
9573
9659
  const finalApprovers = assignsFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
9574
9660
  const explicitRemovals = (removeAssignees || []).filter(Boolean);
9575
9661
  const normalizedProductManagerAssignee = isRealClickUpAssigneeId(productManagerAssignee) ? String(productManagerAssignee).trim() : "";
9576
9662
  const removalTargets = assignsFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
9577
9663
  const assigned = [...new Set([...assignees || [], ...finalApprovers].filter(Boolean))].filter((assignee) => !removalTargets.includes(assignee));
9578
- const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
9664
+ const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask: effectiveIsSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
9579
9665
  const fields = authority ? { merge_authority: JSON.stringify(authority) } : {};
9666
+ if (requiredPullRequest) {
9667
+ fields.github_pull_request = JSON.stringify(requiredPullRequest);
9668
+ fields.github_review_automation = JSON.stringify(reviewAutomation);
9669
+ }
9580
9670
  if (definitionContent) fields.Definition = definitionContent;
9581
9671
  return {
9582
9672
  ok: true,
9583
9673
  dryRun: true,
9584
- transition: { fromStatus: from, toStatus: rule.status, validationPassed, validationFailed, isSubtask },
9674
+ transition: { fromStatus: from, toStatus: rule.status, validationPassed, validationFailed, isSubtask: effectiveIsSubtask },
9675
+ github: requiredPullRequest ? {
9676
+ requiredPullRequest,
9677
+ reviewAutomation
9678
+ } : void 0,
9679
+ vercel: reviewAutomation?.preproduction?.required ? reviewAutomation.preproduction : void 0,
9585
9680
  clickup: {
9586
9681
  status: rule.status,
9587
9682
  assignees: assigned,
@@ -10654,20 +10749,25 @@ function appendDirectoryQuery(url, directory) {
10654
10749
  const separator = url.includes("?") ? "&" : "?";
10655
10750
  return `${url}${separator}directory=${encodeURIComponent(value)}`;
10656
10751
  }
10657
- async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
10752
+ function normalizeOpenCodePromptDelivery(value, fallback = "queue") {
10753
+ const normalized = String(value || "").trim().toLowerCase();
10754
+ return normalized === "steer" || normalized === "queue" ? normalized : fallback;
10755
+ }
10756
+ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false, delivery = "queue" } = {}) {
10658
10757
  if (typeof fetchImpl !== "function") throw new Error("OpenCode direct prompt delivery requires fetch.");
10659
10758
  const root = normalizeOpenCodeBaseUrl(baseUrl, "");
10660
10759
  if (!root) throw new Error("OpenCode direct prompt delivery requires a base URL.");
10661
10760
  const encodedSession = encodeURIComponent(sessionId);
10761
+ const promptDelivery = normalizeOpenCodePromptDelivery(delivery, "queue");
10662
10762
  const attempts = [
10663
10763
  legacyOnly ? null : {
10664
10764
  name: "v2 prompt",
10665
10765
  url: appendDirectoryQuery(`${root}/api/session/${encodedSession}/prompt`, directory),
10666
- body: { prompt: { text }, delivery: "queue", resume: true },
10766
+ body: { prompt: { text }, delivery: promptDelivery, resume: true },
10667
10767
  accept: (response, data) => {
10668
10768
  if (!response.ok) return null;
10669
10769
  if (!openCodePromptAdmissionVerification(data, sessionId)) throw new Error("OpenCode v2 prompt response did not include a valid prompt admission.");
10670
- return { ok: true, method: "http", endpoint: "/api/session/{sessionID}/prompt", status: response.status, data: data.data, response: data };
10770
+ return { ok: true, method: "http", endpoint: "/api/session/{sessionID}/prompt", status: response.status, delivery: promptDelivery, data: data.data, response: data };
10671
10771
  }
10672
10772
  },
10673
10773
  {
@@ -10723,7 +10823,7 @@ async function callOpenCodePromptWithFallbacks(method, sessionId, flatPayload, s
10723
10823
  }
10724
10824
  throw firstError;
10725
10825
  }
10726
- async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false, allowDirectFallback = true } = {}) {
10826
+ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false, allowDirectFallback = true, directDelivery = "queue" } = {}) {
10727
10827
  const directBaseUrl = opencodeBaseUrl || baseUrl;
10728
10828
  const parts = [{ type: "text", text }];
10729
10829
  const flatPayload = { directory, agent, parts };
@@ -10747,7 +10847,7 @@ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, direct
10747
10847
  firstError ??= error;
10748
10848
  }
10749
10849
  }
10750
- if (allowDirectFallback && directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly });
10850
+ if (allowDirectFallback && directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly, delivery: directDelivery });
10751
10851
  if (firstError) throw firstError;
10752
10852
  throw new Error("OpenCode client does not expose session.prompt or session.promptAsync.");
10753
10853
  }
@@ -11120,11 +11220,11 @@ function openCodeBlockingPromptVerification(result, sessionId) {
11120
11220
  if (parts.length > 0 || messageId) return { ok: true, method: parts.length > 0 ? "blocking_prompt_parts" : "blocking_prompt_message", messageId: messageId ? String(messageId) : null, sessionId: deliveredSessionId ? String(deliveredSessionId) : String(sessionId), parts: parts.length };
11121
11221
  return null;
11122
11222
  }
11123
- async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
11223
+ async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, directDelivery = "queue", acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
11124
11224
  const beforeMessages = await readOpenCodeSessionMessages(openCodeClient, { sessionId, directory, limit: 50 }).catch(() => null);
11125
11225
  let sendResult;
11126
11226
  try {
11127
- sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, allowDirectFallback: directPrompt });
11227
+ sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, directDelivery, allowDirectFallback: directPrompt });
11128
11228
  } catch (error) {
11129
11229
  const reason2 = error.message || "message_delivery_failed";
11130
11230
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason: reason2, fallbackAttempted: false });
@@ -11147,7 +11247,13 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
11147
11247
  }
11148
11248
  if (blockingPromptVerification) return { ok: true, verification: blockingPromptVerification, admissionVerification: null, fallback: false };
11149
11249
  if (admissionVerification && acceptPromptAdmission) {
11150
- return { ok: true, verification: admissionVerification, admissionVerification, fallback: false, admissionOnly: true };
11250
+ appendClickUpWebhookLocalLog(worktree, {
11251
+ type: "message_delivery_admission_not_sufficient",
11252
+ taskId,
11253
+ sessionId,
11254
+ admission: admissionVerification,
11255
+ policy: "clickup_routing_requires_visible_delivery"
11256
+ });
11151
11257
  }
11152
11258
  let verification = await verifySessionEventDelivery(openCodeClient, { sessionId, directory, beforeMessages, expectedText: text, markers: eventMarkers });
11153
11259
  if (verification?.ok) return { ok: true, verification, admissionVerification, fallback: false };
@@ -11550,10 +11656,9 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11550
11656
  const sessionId = String(existingSessionId);
11551
11657
  if (await sessionExists(openCodeClient, sessionId, { directory: taskRoute.worktree })) {
11552
11658
  if (typeof clickupClient?.updateTaskMetadata === "function") await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: metadataWithRouting });
11553
- 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 });
11659
+ 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", directDelivery: "steer", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery, applyBlockerOnFailure: false });
11554
11660
  if (!delivery.ok) {
11555
- const recovery2 = await recoverClickUpPmSession({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, staleSessionId: sessionId, sessionTitle, taskRoute, metadataWithRouting, config, prompt, eventMarkers: [taskId, eventType], deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, eventKey, createSession, verifySessionEventDelivery });
11556
- return finish(recovery2);
11661
+ return finish({ ...delivery, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, replacementAttempted: false });
11557
11662
  }
11558
11663
  return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryAdmission: delivery.admissionVerification, deliveryFallback: delivery.fallback, deliveryAttempts: delivery.fallback ? 2 : 1 });
11559
11664
  }
@@ -13766,7 +13871,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
13766
13871
  }
13767
13872
  };
13768
13873
  }
13769
- OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
13874
+ OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpReviewAutomationPlan, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpRequiredPullRequest, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
13770
13875
  export {
13771
13876
  OptimaPlugin as default
13772
13877
  };
@@ -8826,6 +8826,70 @@ function deriveClickUpPrTarget({ isRelease = false, parentTaskId, parentBranch,
8826
8826
  }
8827
8827
  return devBranch;
8828
8828
  }
8829
+ function deriveClickUpRequiredPullRequest({ taskId, taskType = "Tarea", parentTaskId = "", subtaskId = "", branch = "", parentBranch = "", devBranch = "dev", mainBranch = "main", isRelease = false, githubRemote = "origin", prUrl = "", prNumber = "" } = {}) {
8830
+ const effectiveParent = parentTaskId || taskId;
8831
+ const isSubtask = isClickUpSubtaskRoute({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
8832
+ const sourceBranch = branch || deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
8833
+ const targetBranch = deriveClickUpPrTarget({
8834
+ isRelease,
8835
+ parentBranch: isSubtask ? parentBranch || deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent }) : parentBranch,
8836
+ parentTaskId: isSubtask ? effectiveParent : "",
8837
+ parentTaskType: taskType,
8838
+ taskType,
8839
+ devBranch,
8840
+ mainBranch
8841
+ });
8842
+ return {
8843
+ required: true,
8844
+ sourceBranch,
8845
+ targetBranch,
8846
+ githubRemote,
8847
+ prUrl: String(prUrl || "").trim(),
8848
+ prNumber: String(prNumber || "").trim(),
8849
+ scope: isSubtask ? "subtask_to_parent" : isRelease ? "release_to_main" : "task_to_dev",
8850
+ policy: "validation_requires_open_pull_request"
8851
+ };
8852
+ }
8853
+ function buildClickUpReviewAutomationPlan({ isSubtask = false, humanApprover = "CTO", preproductionProvider = "Vercel", preproductionEnvironment = "preproduction" } = {}) {
8854
+ return {
8855
+ github_webhooks: {
8856
+ listen_for: ["pull_request_review", "pull_request_review_comment", "issue_comment", "pull_request"],
8857
+ comment_policy: "reply_before_change_and_after_change",
8858
+ change_request_status: "in progress",
8859
+ after_fix_status: "validation",
8860
+ state_storage: "ClickUp agent_metadata.github"
8861
+ },
8862
+ review_comments: [
8863
+ "Answer GitHub comments directly in GitHub.",
8864
+ "If a comment implies a code/doc/test change, first reply with the intended action, move the ClickUp task/subtask to in progress, implement, push the same branch, move it back to validation, update the PR, then reply again with what changed.",
8865
+ "Also update ClickUp with human-readable model work status; runtime/process noise stays in local logs."
8866
+ ],
8867
+ accepted_review: {
8868
+ accepted_by: humanApprover,
8869
+ trigger: "approved_pull_request_review",
8870
+ action: isSubtask ? "merge_subtask_pr_into_parent_branch" : "merge_parent_pr_into_dev",
8871
+ delete_source_branch_after_success: true,
8872
+ delete_worktree_after_success: true,
8873
+ preserve_opencode_sessions_in_agent_metadata: true
8874
+ },
8875
+ preproduction: isSubtask ? {
8876
+ required: false,
8877
+ reason: "Subtask merge stops at parent branch; parent task owns dev/preproduction."
8878
+ } : {
8879
+ required: true,
8880
+ provider: preproductionProvider,
8881
+ environment: preproductionEnvironment,
8882
+ verify_deployment_after_merge: true,
8883
+ run_smoke_regression: true,
8884
+ on_failure: "create_bug_subtasks_and_return_parent_to_in_progress",
8885
+ completion_gate: "preproduction_deployed_and_regression_passed"
8886
+ },
8887
+ completion: {
8888
+ final_clickup_comment_required: true,
8889
+ final_status: isSubtask ? "completed_or_parent_validation" : "completed"
8890
+ }
8891
+ };
8892
+ }
8829
8893
  function validateMainWorkspaceBranchSafety({ currentBranch, requiredBranch = "dev", forbiddenBranch = "main" } = {}) {
8830
8894
  const normalized = String(currentBranch ?? "").trim();
8831
8895
  if (!normalized) {
@@ -9488,6 +9552,7 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9488
9552
  const worktree = deriveClickUpWorktree({ baseWorktree, taskId, taskType, parentTaskId: effectiveParent, subtaskId });
9489
9553
  const prTarget = isSubtask ? parentBranch : "dev";
9490
9554
  const startFrom = isSubtask ? parentBranch : "dev";
9555
+ const requiredPullRequest = deriveClickUpRequiredPullRequest({ taskId, taskType, parentTaskId: effectiveParent, subtaskId, branch, parentBranch });
9491
9556
  const estimate = preEstimateClickUpWork({ complexity, filesChanged, acceptanceCriteria, unknowns });
9492
9557
  const metadata = mergeClickUpAgentMetadata(existingAgentMetadata, {
9493
9558
  task: {
@@ -9501,6 +9566,10 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9501
9566
  mirror_task_path: `.optima/tasks/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}.md`,
9502
9567
  evidence_path: `.optima/evidences/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}/SUMMARY.md`,
9503
9568
  delivery_evidence_path: deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent),
9569
+ github: {
9570
+ required_pull_request: requiredPullRequest,
9571
+ review_automation: buildClickUpReviewAutomationPlan({ isSubtask })
9572
+ },
9504
9573
  ...agentMetadata
9505
9574
  }
9506
9575
  });
@@ -9523,6 +9592,10 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9523
9592
  parentBranch: parentBranch || void 0,
9524
9593
  prTarget
9525
9594
  },
9595
+ github: {
9596
+ requiredPullRequest,
9597
+ reviewAutomation: buildClickUpReviewAutomationPlan({ isSubtask })
9598
+ },
9526
9599
  mirror: {
9527
9600
  taskPath: `.optima/tasks/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}.md`,
9528
9601
  evidenceSummaryPath: `.optima/evidences/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}/SUMMARY.md`,
@@ -9533,14 +9606,15 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9533
9606
  wouldCreate: true
9534
9607
  },
9535
9608
  clickup: {
9536
- comment: `Starting task from ${startFrom}. Branch: ${branch}. Worktree: ${worktree}. Delivery evidence: ${deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent)}.`,
9609
+ comment: `Starting task from ${startFrom}. Branch: ${branch}. Worktree: ${worktree}. Validation PR target: ${requiredPullRequest.targetBranch}. Delivery evidence: ${deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent)}.`,
9537
9610
  description,
9538
9611
  fields: {
9539
9612
  Definition: definitionContent,
9540
9613
  "Story Points": estimate.storyPoints,
9541
9614
  agent_metadata: metadata,
9542
9615
  branch,
9543
- worktree
9616
+ worktree,
9617
+ pr_target: requiredPullRequest.targetBranch
9544
9618
  },
9545
9619
  definition_doc: {
9546
9620
  parent: normalizeClickUpDefinitionDocParent(definitionDocParent),
@@ -9550,23 +9624,29 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
9550
9624
  }
9551
9625
  };
9552
9626
  }
9553
- function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed = false, validationFailed = false, isSubtask = false, reopen = false, assignees = [], removeAssignees = [], productManagerAssignee = "", requireProductManagerAssignee = true, planDescription = "", definition = "", definitionDocParent = CLICKUP_DEFINITION_DOC_PARENT, humansRegistry } = {}) {
9627
+ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed = false, validationFailed = false, isSubtask = false, reopen = false, assignees = [], removeAssignees = [], productManagerAssignee = "", requireProductManagerAssignee = true, planDescription = "", definition = "", definitionDocParent = CLICKUP_DEFINITION_DOC_PARENT, humansRegistry, taskId = "", taskType = "Tarea", parentTaskId = "", subtaskId = "", branch = "", parentBranch = "", prUrl = "", prNumber = "", githubRemote = "origin", devBranch = "dev", mainBranch = "main", preproductionProvider = "Vercel", preproductionEnvironment = "preproduction" } = {}) {
9554
9628
  const from = normalizeClickUpStatus(fromStatus);
9629
+ const effectiveIsSubtask = Boolean(isSubtask || isClickUpSubtaskRoute({ taskType, parentTaskId: parentTaskId || taskId, subtaskId, taskId }));
9555
9630
  let to = normalizeClickUpStatus(toStatus);
9556
9631
  if (validationFailed) to = "in progress";
9557
- if (!to && validationPassed) to = isSubtask ? "completed" : "merge";
9632
+ if (!to && validationPassed) to = effectiveIsSubtask ? "completed" : "merge";
9558
9633
  if ((from === "completed" || from === "closed") && !reopen) {
9559
9634
  return { ok: true, noop: true, dryRun: true, message: "Completed/closed task is a no-op unless reopen is explicit." };
9560
9635
  }
9561
9636
  const key = `${from}->${to}`;
9562
9637
  const rule = CLICKUP_TRANSITIONS.get(key);
9563
9638
  if (!rule) return { ok: false, dryRun: true, message: `Transition not allowed: ${key}` };
9564
- const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && isSubtask);
9639
+ const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && effectiveIsSubtask);
9565
9640
  const requiresPlanCompletionContract = from === "plan" && to === "in progress";
9641
+ const requiresValidationPullRequest = from === "in progress" && to === "validation";
9566
9642
  const definitionContent = compactMarkdownValue(definition);
9567
9643
  const description = compactMarkdownValue(planDescription);
9644
+ const canDerivePullRequest = Boolean(taskId || branch);
9645
+ const shouldAttachPullRequest = requiresValidationPullRequest || (from === "validation" || to === "merge" || from === "merge") && canDerivePullRequest;
9646
+ const requiredPullRequest = shouldAttachPullRequest && canDerivePullRequest ? deriveClickUpRequiredPullRequest({ taskId, taskType, parentTaskId, subtaskId, branch, parentBranch, devBranch, mainBranch, githubRemote, prUrl, prNumber }) : null;
9647
+ const reviewAutomation = requiredPullRequest ? buildClickUpReviewAutomationPlan({ isSubtask: effectiveIsSubtask, preproductionProvider, preproductionEnvironment }) : null;
9568
9648
  const validationErrors = [];
9569
- if (rule.parentOnlyFinalApproval && isSubtask) {
9649
+ if (rule.parentOnlyFinalApproval && effectiveIsSubtask) {
9570
9650
  validationErrors.push("Subtasks must not use the parent final-approval transition; merge validated subtasks directly into the parent branch/workspace.");
9571
9651
  }
9572
9652
  if (assignsFinalApprovers && !isRealClickUpAssigneeId(productManagerAssignee) && requireProductManagerAssignee !== false) {
@@ -9576,19 +9656,34 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9576
9656
  if (!description) validationErrors.push("planDescription is required for the plan-completion ClickUp task description rewrite.");
9577
9657
  if (!definitionContent) validationErrors.push("definition is required and must contain the complete plan/Definition content at plan completion.");
9578
9658
  }
9659
+ if (requiresValidationPullRequest) {
9660
+ if (!requiredPullRequest) validationErrors.push("taskId or branch is required before moving a ClickUp task to validation.");
9661
+ if (!requiredPullRequest?.sourceBranch) validationErrors.push("branch/sourceBranch is required before moving a ClickUp task to validation.");
9662
+ if (!requiredPullRequest?.targetBranch) validationErrors.push("PR target branch is required before moving a ClickUp task to validation.");
9663
+ if (!requiredPullRequest?.prUrl && !requiredPullRequest?.prNumber) validationErrors.push("An open GitHub pull request URL or number is required before moving a ClickUp task to validation.");
9664
+ }
9579
9665
  if (validationErrors.length > 0) return clickUpPayloadValidationError(validationErrors);
9580
9666
  const finalApprovers = assignsFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
9581
9667
  const explicitRemovals = (removeAssignees || []).filter(Boolean);
9582
9668
  const normalizedProductManagerAssignee = isRealClickUpAssigneeId(productManagerAssignee) ? String(productManagerAssignee).trim() : "";
9583
9669
  const removalTargets = assignsFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
9584
9670
  const assigned = [...new Set([...assignees || [], ...finalApprovers].filter(Boolean))].filter((assignee) => !removalTargets.includes(assignee));
9585
- const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
9671
+ const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask: effectiveIsSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
9586
9672
  const fields = authority ? { merge_authority: JSON.stringify(authority) } : {};
9673
+ if (requiredPullRequest) {
9674
+ fields.github_pull_request = JSON.stringify(requiredPullRequest);
9675
+ fields.github_review_automation = JSON.stringify(reviewAutomation);
9676
+ }
9587
9677
  if (definitionContent) fields.Definition = definitionContent;
9588
9678
  return {
9589
9679
  ok: true,
9590
9680
  dryRun: true,
9591
- transition: { fromStatus: from, toStatus: rule.status, validationPassed, validationFailed, isSubtask },
9681
+ transition: { fromStatus: from, toStatus: rule.status, validationPassed, validationFailed, isSubtask: effectiveIsSubtask },
9682
+ github: requiredPullRequest ? {
9683
+ requiredPullRequest,
9684
+ reviewAutomation
9685
+ } : void 0,
9686
+ vercel: reviewAutomation?.preproduction?.required ? reviewAutomation.preproduction : void 0,
9592
9687
  clickup: {
9593
9688
  status: rule.status,
9594
9689
  assignees: assigned,
@@ -10661,20 +10756,25 @@ function appendDirectoryQuery(url, directory) {
10661
10756
  const separator = url.includes("?") ? "&" : "?";
10662
10757
  return `${url}${separator}directory=${encodeURIComponent(value)}`;
10663
10758
  }
10664
- async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
10759
+ function normalizeOpenCodePromptDelivery(value, fallback = "queue") {
10760
+ const normalized = String(value || "").trim().toLowerCase();
10761
+ return normalized === "steer" || normalized === "queue" ? normalized : fallback;
10762
+ }
10763
+ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false, delivery = "queue" } = {}) {
10665
10764
  if (typeof fetchImpl !== "function") throw new Error("OpenCode direct prompt delivery requires fetch.");
10666
10765
  const root = normalizeOpenCodeBaseUrl(baseUrl, "");
10667
10766
  if (!root) throw new Error("OpenCode direct prompt delivery requires a base URL.");
10668
10767
  const encodedSession = encodeURIComponent(sessionId);
10768
+ const promptDelivery = normalizeOpenCodePromptDelivery(delivery, "queue");
10669
10769
  const attempts = [
10670
10770
  legacyOnly ? null : {
10671
10771
  name: "v2 prompt",
10672
10772
  url: appendDirectoryQuery(`${root}/api/session/${encodedSession}/prompt`, directory),
10673
- body: { prompt: { text }, delivery: "queue", resume: true },
10773
+ body: { prompt: { text }, delivery: promptDelivery, resume: true },
10674
10774
  accept: (response, data) => {
10675
10775
  if (!response.ok) return null;
10676
10776
  if (!openCodePromptAdmissionVerification(data, sessionId)) throw new Error("OpenCode v2 prompt response did not include a valid prompt admission.");
10677
- return { ok: true, method: "http", endpoint: "/api/session/{sessionID}/prompt", status: response.status, data: data.data, response: data };
10777
+ return { ok: true, method: "http", endpoint: "/api/session/{sessionID}/prompt", status: response.status, delivery: promptDelivery, data: data.data, response: data };
10678
10778
  }
10679
10779
  },
10680
10780
  {
@@ -10730,7 +10830,7 @@ async function callOpenCodePromptWithFallbacks(method, sessionId, flatPayload, s
10730
10830
  }
10731
10831
  throw firstError;
10732
10832
  }
10733
- async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false, allowDirectFallback = true } = {}) {
10833
+ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false, allowDirectFallback = true, directDelivery = "queue" } = {}) {
10734
10834
  const directBaseUrl = opencodeBaseUrl || baseUrl;
10735
10835
  const parts = [{ type: "text", text }];
10736
10836
  const flatPayload = { directory, agent, parts };
@@ -10754,7 +10854,7 @@ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, direct
10754
10854
  firstError ??= error;
10755
10855
  }
10756
10856
  }
10757
- if (allowDirectFallback && directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly });
10857
+ if (allowDirectFallback && directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly, delivery: directDelivery });
10758
10858
  if (firstError) throw firstError;
10759
10859
  throw new Error("OpenCode client does not expose session.prompt or session.promptAsync.");
10760
10860
  }
@@ -11127,11 +11227,11 @@ function openCodeBlockingPromptVerification(result, sessionId) {
11127
11227
  if (parts.length > 0 || messageId) return { ok: true, method: parts.length > 0 ? "blocking_prompt_parts" : "blocking_prompt_message", messageId: messageId ? String(messageId) : null, sessionId: deliveredSessionId ? String(deliveredSessionId) : String(sessionId), parts: parts.length };
11128
11228
  return null;
11129
11229
  }
11130
- async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
11230
+ async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, directDelivery = "queue", acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
11131
11231
  const beforeMessages = await readOpenCodeSessionMessages(openCodeClient, { sessionId, directory, limit: 50 }).catch(() => null);
11132
11232
  let sendResult;
11133
11233
  try {
11134
- sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, allowDirectFallback: directPrompt });
11234
+ sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, directDelivery, allowDirectFallback: directPrompt });
11135
11235
  } catch (error) {
11136
11236
  const reason2 = error.message || "message_delivery_failed";
11137
11237
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason: reason2, fallbackAttempted: false });
@@ -11154,7 +11254,13 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
11154
11254
  }
11155
11255
  if (blockingPromptVerification) return { ok: true, verification: blockingPromptVerification, admissionVerification: null, fallback: false };
11156
11256
  if (admissionVerification && acceptPromptAdmission) {
11157
- return { ok: true, verification: admissionVerification, admissionVerification, fallback: false, admissionOnly: true };
11257
+ appendClickUpWebhookLocalLog(worktree, {
11258
+ type: "message_delivery_admission_not_sufficient",
11259
+ taskId,
11260
+ sessionId,
11261
+ admission: admissionVerification,
11262
+ policy: "clickup_routing_requires_visible_delivery"
11263
+ });
11158
11264
  }
11159
11265
  let verification = await verifySessionEventDelivery(openCodeClient, { sessionId, directory, beforeMessages, expectedText: text, markers: eventMarkers });
11160
11266
  if (verification?.ok) return { ok: true, verification, admissionVerification, fallback: false };
@@ -11557,10 +11663,9 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11557
11663
  const sessionId = String(existingSessionId);
11558
11664
  if (await sessionExists(openCodeClient, sessionId, { directory: taskRoute.worktree })) {
11559
11665
  if (typeof clickupClient?.updateTaskMetadata === "function") await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: metadataWithRouting });
11560
- 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 });
11666
+ 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", directDelivery: "steer", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery, applyBlockerOnFailure: false });
11561
11667
  if (!delivery.ok) {
11562
- const recovery2 = await recoverClickUpPmSession({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, staleSessionId: sessionId, sessionTitle, taskRoute, metadataWithRouting, config, prompt, eventMarkers: [taskId, eventType], deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, eventKey, createSession, verifySessionEventDelivery });
11563
- return finish(recovery2);
11668
+ return finish({ ...delivery, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, replacementAttempted: false });
11564
11669
  }
11565
11670
  return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryAdmission: delivery.admissionVerification, deliveryFallback: delivery.fallback, deliveryAttempts: delivery.fallback ? 2 : 1 });
11566
11671
  }
@@ -13773,7 +13878,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
13773
13878
  }
13774
13879
  };
13775
13880
  }
13776
- OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
13881
+ OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpReviewAutomationPlan, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpRequiredPullRequest, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
13777
13882
 
13778
13883
  // src/sanitize_cli.js
13779
13884
  var { migrateLegacyOptimaLayout: migrateLegacyOptimaLayout2 } = OptimaPlugin.__internals;
@@ -61,3 +61,14 @@ Use shared policies and additive agent files by default. Do not create or popula
61
61
  - PMA links the task to an approved SCR.
62
62
  - Architect helps decompose the work into slice-based subtasks.
63
63
  - `workflow_runner` executes the end-to-end delivery cycle through specialist delegation while PMA waits for completion notification.
64
+
65
+ ## ClickUp-First PR And Preproduction Gate
66
+
67
+ For ClickUp-first delivery, Validation is a GitHub PR state, not a comment-only handoff.
68
+
69
+ - Subtasks open/update a PR from the subtask branch into the parent task branch before entering Validation.
70
+ - Parent tasks open/update a PR from the task branch into `dev` before entering Validation.
71
+ - GitHub review/comment webhooks wake the workflow owner. The agent replies in GitHub; if a comment requires a change, it moves ClickUp back to `in progress`, fixes/pushes the same branch, returns ClickUp to `validation`, updates the PR, and replies again with the result.
72
+ - The configured final approver/CTO approving the parent PR is the merge trigger. After merge to `dev`, Vercel preproduction must deploy automatically and pass a small smoke/regression check before cleanup and ClickUp `completed`.
73
+ - If merge, Vercel deployment, or regression fails, create Bug subtasks under the parent and return the parent to `in progress`.
74
+ - Worktrees/branches may be deleted only after the merge and preproduction gate pass. OpenCode session ids remain in ClickUp `agent_metadata` so a reopened task can recreate the worktree and resume context.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defend-tech/opencode-optima",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+ssh://git@github.com/defend-tech/opencode-optima.git"
@@ -67,5 +67,24 @@ workflow:
67
67
  subtask: parent_branch
68
68
  parent: dev
69
69
  release: main
70
+ validation_requires_pull_request: true # A task/subtask cannot enter Validation without an open GitHub PR
71
+ review_webhooks:
72
+ enabled: true
73
+ events:
74
+ - pull_request_review
75
+ - pull_request_review_comment
76
+ - issue_comment
77
+ - pull_request
78
+ comment_policy: reply_before_change_and_after_change
79
+ change_request_status: in progress
80
+ after_fix_status: validation
81
+ accepted_by: CTO
82
+ vercel:
83
+ preproduction:
84
+ required_for_parent_tasks: true
85
+ environment: preproduction
86
+ verify_deployment_after_merge: true
87
+ smoke_regression_required: true
88
+ on_failure: create_bug_subtasks_and_return_parent_to_in_progress
70
89
 
71
90
  agents: