@defend-tech/opencode-optima 0.1.72 → 0.1.74

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,8 @@ 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`: 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.
48
+ - `validation`: before moving any task/subtask into Validation, create or update the required GitHub PR, store its URL/number in ClickUp `agent_metadata`, and leave a concise model-owned ClickUp status comment with the PR link, source branch, target branch, and current validation owner. 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 link visible in ClickUp is invalid: move it back to `in progress`, open/update the PR, post/update the ClickUp PR-link status, 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; every validation/update status must include the current PR link. Never post Optima runtime/process noise.
50
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.
51
51
  - `completed` / `Closed`: no execution unless explicitly reopened.
52
52
 
@@ -62,7 +62,7 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
62
62
  - Parent setup pulls remote once; after parent branch creation, subtasks can trust the parent local branch without continuous remote polling.
63
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.
64
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.
65
+ - Required PR gate: every transition into `validation` must have an open GitHub PR and a ClickUp-visible model status comment containing that PR link. Do not mark Validation with only local commits, evidence, or comments without the PR URL.
66
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
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`.
68
68
  - Preserve user work and unrelated dirty files. Stop and ask if unexpected changes appear.
package/dist/index.js CHANGED
@@ -8627,6 +8627,7 @@ var activeWorkflows = /* @__PURE__ */ new Map();
8627
8627
  var activeClickUpWebhookListeners = /* @__PURE__ */ new Map();
8628
8628
  var activeClickUpWebhookLifecycleRegistry = /* @__PURE__ */ new Map();
8629
8629
  var activeClickUpTaskRoutes = /* @__PURE__ */ new Map();
8630
+ var GITHUB_WEBHOOK_EVENTS = ["pull_request", "pull_request_review", "pull_request_review_comment", "issue_comment"];
8630
8631
  function isRootDirectory(candidate) {
8631
8632
  const resolved = path6.resolve(candidate);
8632
8633
  return resolved === path6.parse(resolved).root;
@@ -9880,9 +9881,48 @@ function normalizeNonNegativeInteger(value, defaultValue) {
9880
9881
  const number = Number(value);
9881
9882
  return Number.isInteger(number) && number >= 0 ? number : defaultValue;
9882
9883
  }
9884
+ function defaultGitHubWebhookPublicUrl(clickupPublicUrl = "") {
9885
+ const raw = String(clickupPublicUrl || "").trim();
9886
+ if (!raw) return "";
9887
+ try {
9888
+ const url = new URL(raw);
9889
+ url.pathname = "/optima/github/webhook";
9890
+ url.search = "";
9891
+ url.hash = "";
9892
+ return url.toString();
9893
+ } catch {
9894
+ return raw.replace(/\/optima\/clickup\/webhook\b/, "/optima/github/webhook");
9895
+ }
9896
+ }
9897
+ function normalizeGitHubWebhookConfig(rawGithub = {}, clickupWebhook = {}) {
9898
+ const github = isPlainObject(rawGithub) ? rawGithub : {};
9899
+ const webhook = isPlainObject(github.webhook) ? github.webhook : {};
9900
+ const enabled = github.enabled === true || webhook.enabled === true;
9901
+ const events = Array.isArray(webhook.events) && webhook.events.length > 0 ? [...new Set(webhook.events.map((event) => String(event || "").trim()).filter(Boolean))] : [...GITHUB_WEBHOOK_EVENTS];
9902
+ const publicUrl = String(webhook.public_url || webhook.publicUrl || github.public_url || github.publicUrl || defaultGitHubWebhookPublicUrl(clickupWebhook.publicUrl)).trim();
9903
+ const secret = String(webhook.secret || github.secret || "").trim();
9904
+ const apiToken = String(github.api_token || github.apiToken || "").trim();
9905
+ const repository = String(github.repository || github.repo || "").trim();
9906
+ const [ownerFromRepo, nameFromRepo] = repository.includes("/") ? repository.split("/", 2) : ["", ""];
9907
+ return {
9908
+ enabled,
9909
+ apiToken,
9910
+ repository,
9911
+ owner: String(github.owner || ownerFromRepo || "").trim(),
9912
+ repo: String(github.repo_name || github.repoName || nameFromRepo || "").trim(),
9913
+ webhook: {
9914
+ enabled,
9915
+ publicUrl,
9916
+ secret,
9917
+ path: publicUrl ? new URL(publicUrl, "http://localhost").pathname : "/optima/github/webhook",
9918
+ events
9919
+ }
9920
+ };
9921
+ }
9883
9922
  function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd()) {
9884
9923
  const raw = isPlainObject(rawClickUp) ? rawClickUp : {};
9885
9924
  const webhook = isPlainObject(raw.webhook) ? raw.webhook : {};
9925
+ const github = normalizeGitHubWebhookConfig(raw.github, { publicUrl: String(webhook.public_url || webhook.publicUrl || "").trim() });
9886
9926
  const routing = isPlainObject(raw.routing) ? raw.routing : {};
9887
9927
  const opencode = isPlainObject(raw.opencode) ? raw.opencode : {};
9888
9928
  const openchamber = isPlainObject(raw.openchamber) ? raw.openchamber : {};
@@ -9935,6 +9975,7 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9935
9975
  },
9936
9976
  events
9937
9977
  },
9978
+ github,
9938
9979
  routing: {
9939
9980
  targetAgent: String(routing.target_agent || routing.targetAgent || "workflow_product_manager").trim(),
9940
9981
  productManagerAssigneeId: String(routing.product_manager_assignee_id || routing.productManagerAssigneeId || "").trim(),
@@ -9956,6 +9997,13 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9956
9997
  if (!config.webhook.bindHost) errors.push("clickup.webhook.bind_host is required");
9957
9998
  if (!Number.isInteger(config.webhook.bindPort) || config.webhook.bindPort <= 0) errors.push("clickup.webhook.bind_port must be a positive integer");
9958
9999
  if (!config.webhook.location.list_id && !config.webhook.location.folder_id && !config.webhook.location.space_id) errors.push("one clickup.webhook.location id is required");
10000
+ if (config.github.webhook.enabled) {
10001
+ if (!config.github.webhook.publicUrl) errors.push("clickup.github.webhook.public_url is required when GitHub webhook is enabled");
10002
+ if (!config.github.webhook.secret) errors.push("clickup.github.webhook.secret is required when GitHub webhook is enabled");
10003
+ if (!config.github.webhook.path || config.github.webhook.path === clickUpWebhookExpectedPath(config)) errors.push("clickup.github.webhook.path must be distinct from the ClickUp webhook path");
10004
+ const missingGitHubEvents = GITHUB_WEBHOOK_EVENTS.filter((event) => !config.github.webhook.events.includes(event));
10005
+ if (missingGitHubEvents.length > 0) errors.push(`clickup.github.webhook.events missing: ${missingGitHubEvents.join(", ")}`);
10006
+ }
9959
10007
  if (config.opencode.baseUrl) {
9960
10008
  try {
9961
10009
  new URL(config.opencode.baseUrl);
@@ -9993,6 +10041,8 @@ function sanitizeClickUpWebhookState(state = {}, config = null) {
9993
10041
  listener: isPlainObject(state.listener) ? state.listener : {},
9994
10042
  pendingSessions: isPlainObject(state.pendingSessions) ? state.pendingSessions : {},
9995
10043
  lastWebhookAt: state.lastWebhookAt || state.last_webhook_at || null,
10044
+ githubLastWebhookAt: state.githubLastWebhookAt || state.github_last_webhook_at || null,
10045
+ githubRecentEventKeys: Array.isArray(state.githubRecentEventKeys) ? state.githubRecentEventKeys.slice(-200) : [],
9996
10046
  recentEventKeys
9997
10047
  };
9998
10048
  }
@@ -10109,6 +10159,29 @@ function createClickUpApiClient(config, fetchImpl = globalThis.fetch) {
10109
10159
  }
10110
10160
  };
10111
10161
  }
10162
+ function createGitHubApiClient(config = {}, fetchImpl = globalThis.fetch) {
10163
+ const token = resolveSecretReference(config?.apiToken);
10164
+ const owner = String(config?.owner || "").trim();
10165
+ const repo = String(config?.repo || "").trim();
10166
+ const request = async (pathname) => {
10167
+ if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable; inject a GitHub client for live PR lookup");
10168
+ if (!owner || !repo) throw new Error("GitHub repository owner/repo is not configured");
10169
+ const response = await fetchImpl(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}${pathname}`, {
10170
+ headers: {
10171
+ Accept: "application/vnd.github+json",
10172
+ "X-GitHub-Api-Version": "2022-11-28",
10173
+ ...token ? { Authorization: `Bearer ${token}` } : {}
10174
+ }
10175
+ });
10176
+ if (!response.ok) throw new Error(`GitHub API request failed: ${response.status}`);
10177
+ return response.json();
10178
+ };
10179
+ return {
10180
+ async getPullRequest(number) {
10181
+ return request(`/pulls/${encodeURIComponent(number)}`);
10182
+ }
10183
+ };
10184
+ }
10112
10185
  function createTestClickUpApiClient(config) {
10113
10186
  const metadata = /* @__PURE__ */ new Map();
10114
10187
  return {
@@ -10339,6 +10412,19 @@ function verifyClickUpSignature(rawBody, signatureHeader, secret) {
10339
10412
  const wanted = Buffer.from(expected, "hex");
10340
10413
  return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
10341
10414
  }
10415
+ function verifyGitHubSignature(rawBody, signatureHeader, secret) {
10416
+ return verifyClickUpSignature(rawBody, signatureHeader, secret);
10417
+ }
10418
+ function gitHubWebhookEventKey({ headers = {}, event = "", payload = {} } = {}) {
10419
+ const delivery = headers["x-github-delivery"] || headers["X-GitHub-Delivery"] || payload.delivery || payload.delivery_id || "";
10420
+ const id = delivery || payload.pull_request?.id || payload.comment?.id || payload.review?.id || payload.issue?.id || "unknown";
10421
+ return ["github", event || headers["x-github-event"] || headers["X-GitHub-Event"] || "event", id].filter(Boolean).join(":");
10422
+ }
10423
+ function rememberGitHubWebhookEvent(state = {}, eventKey, limit = 200) {
10424
+ const recent = Array.isArray(state.githubRecentEventKeys) ? state.githubRecentEventKeys : [];
10425
+ if (recent.includes(eventKey)) return { duplicate: true, state };
10426
+ return { duplicate: false, state: { ...state, githubRecentEventKeys: [...recent, eventKey].slice(-limit) } };
10427
+ }
10342
10428
  async function resyncClickUpWebhookForSignatureDrift({ rawBody, signature, config, state = {}, worktree = process.cwd(), clickupClient, saveState } = {}) {
10343
10429
  const validation = clickUpWebhookValidationFromConfig(config);
10344
10430
  if (!clickupClient?.listWebhooks && !clickupClient?.createWebhook) {
@@ -10545,6 +10631,105 @@ function clickUpTaskAgentMetadata(task = {}, fieldId = "") {
10545
10631
  const field = fields.find((item) => String(item?.id || "") === fieldId || String(item?.name || "") === "agent_metadata");
10546
10632
  return field?.value || "";
10547
10633
  }
10634
+ function gitHubWebhookExpectedPath(config = {}) {
10635
+ return config?.github?.webhook?.path || "/optima/github/webhook";
10636
+ }
10637
+ function gitHubWebhookEnabled(config = {}) {
10638
+ return Boolean(config?.github?.webhook?.enabled && resolveSecretReference(config?.github?.webhook?.secret));
10639
+ }
10640
+ function clickUpTaskIdFromGitHubBranch(branch = "") {
10641
+ const tail = String(branch || "").trim().split("/").filter(Boolean).at(-1) || "";
10642
+ if (!tail) return "";
10643
+ const subtask = tail.match(/-subtask-(.+)$/);
10644
+ return branchSafeClickUpId(subtask ? subtask[1] : tail);
10645
+ }
10646
+ async function gitHubPullRequestContextFromPayload(payload = {}, event = "", githubClient = null) {
10647
+ let pr = payload.pull_request || null;
10648
+ if (!pr && event === "issue_comment" && payload.issue?.pull_request && githubClient?.getPullRequest) {
10649
+ pr = await githubClient.getPullRequest(payload.issue.number);
10650
+ }
10651
+ if (!pr) return null;
10652
+ const headRef = String(pr.head?.ref || pr.head_ref || "").trim();
10653
+ const baseRef = String(pr.base?.ref || pr.base_ref || "").trim();
10654
+ return {
10655
+ number: pr.number || payload.issue?.number || "",
10656
+ id: pr.id || "",
10657
+ url: pr.html_url || pr.url || "",
10658
+ title: pr.title || payload.issue?.title || "",
10659
+ state: pr.state || "",
10660
+ merged: pr.merged === true,
10661
+ headRef,
10662
+ baseRef,
10663
+ taskId: clickUpTaskIdFromGitHubBranch(headRef),
10664
+ repository: pr.head?.repo?.full_name || pr.base?.repo?.full_name || payload.repository?.full_name || ""
10665
+ };
10666
+ }
10667
+ function gitHubWebhookCommentBody(payload = {}) {
10668
+ return String(payload.comment?.body || payload.review?.body || payload.pull_request?.body || "").trim();
10669
+ }
10670
+ function formatGitHubWebhookPrompt({ event = "", action = "", pr = {}, payload = {}, taskId = "" } = {}) {
10671
+ const sender = payload.sender?.login || payload.comment?.user?.login || payload.review?.user?.login || "unknown";
10672
+ const reviewState = String(payload.review?.state || "").trim();
10673
+ const commentBody = gitHubWebhookCommentBody(payload);
10674
+ const lines = [
10675
+ "[GitHub Webhook Event]",
10676
+ `Event: ${event}`,
10677
+ `Action: ${action || "unknown"}`,
10678
+ `Task ID: ${taskId || pr.taskId || "unknown"}`,
10679
+ `Pull Request: #${pr.number || "unknown"} ${pr.url || ""}`.trim(),
10680
+ `PR Branch: ${pr.headRef || "unknown"} -> ${pr.baseRef || "unknown"}`,
10681
+ `Sender: ${sender}`,
10682
+ reviewState ? `Review State: ${reviewState}` : null,
10683
+ commentBody ? ["", "Comment/Review Body:", commentBody].join("\n") : null,
10684
+ "",
10685
+ "Required handling:",
10686
+ "- Answer human GitHub comments in GitHub.",
10687
+ "- If the comment/review implies a change, reply first with what you will do, move the ClickUp task/subtask to in progress, implement/fix in the same branch, push, return ClickUp to validation, update the PR, and reply again with what changed.",
10688
+ "- If this is an approved parent PR by the configured final approver/CTO, continue the merge-to-dev, Vercel preproduction verification, smoke regression, cleanup, and ClickUp completion workflow.",
10689
+ "- Keep Optima runtime/process failures out of ClickUp comments; use local logs for process noise."
10690
+ ].filter(Boolean);
10691
+ return lines.join("\n");
10692
+ }
10693
+ function mergeGitHubWebhookMetadata(existingMetadata, { pr = {}, event = "", action = "", delivery = "" } = {}) {
10694
+ const current = normalizeAgentMetadataJson(existingMetadata);
10695
+ const next = {
10696
+ ...current,
10697
+ task: {
10698
+ ...isPlainObject(current.task) ? current.task : {},
10699
+ github: {
10700
+ ...isPlainObject(current.task?.github) ? current.task.github : {},
10701
+ pull_request: Object.fromEntries(Object.entries({
10702
+ number: pr.number || void 0,
10703
+ url: pr.url || void 0,
10704
+ source_branch: pr.headRef || void 0,
10705
+ target_branch: pr.baseRef || void 0,
10706
+ repository: pr.repository || void 0
10707
+ }).filter(([, value]) => value !== void 0 && value !== "")),
10708
+ last_webhook: {
10709
+ event,
10710
+ action,
10711
+ delivery,
10712
+ at: (/* @__PURE__ */ new Date()).toISOString()
10713
+ }
10714
+ }
10715
+ }
10716
+ };
10717
+ return JSON.stringify(sortJsonValue(next), null, 2);
10718
+ }
10719
+ function summarizeGitHubRouteForLog(result = {}, payload = {}, event = "") {
10720
+ return {
10721
+ taskId: result.taskId || null,
10722
+ event: event || null,
10723
+ eventKey: result.eventKey || null,
10724
+ ok: result.ok === true,
10725
+ action: result.action || null,
10726
+ reason: result.reason || null,
10727
+ sessionId: result.sessionId || null,
10728
+ pullRequest: result.prNumber || payload?.pull_request?.number || payload?.issue?.number || null,
10729
+ branch: result.branch || null,
10730
+ worktree: result.worktree || null
10731
+ };
10732
+ }
10548
10733
  function getNestedMetadataValue(metadata, dottedKey) {
10549
10734
  const parts = String(dottedKey || "").split(".").filter(Boolean);
10550
10735
  let cursor = metadata;
@@ -11450,9 +11635,9 @@ function buildClickUpWebhookAuditSummary({ method, url, config, handled, error,
11450
11635
  ok: Boolean(handled?.ok),
11451
11636
  reason: handled?.reason || result.reason || (error ? "handler_error" : void 0),
11452
11637
  action: result.action,
11453
- event: payload ? clickUpEventType(payload) : void 0,
11638
+ event: result.event || (payload ? clickUpEventType(payload) : void 0),
11454
11639
  task: result.taskId || clickUpTaskIdFromPayload(payload || {}),
11455
- comment: clickUpCommentFromPayload(payload || {})?.id || payload?.comment_id || payload?.commentId,
11640
+ comment: clickUpCommentFromPayload(payload || {})?.id || payload?.comment_id || payload?.commentId || payload?.comment?.id || payload?.review?.id,
11456
11641
  session: result.sessionId,
11457
11642
  base_path: config?.basePath || "",
11458
11643
  request_file: requestFile || void 0
@@ -11466,7 +11651,13 @@ function writeClickUpWebhookAuditLog({ method, url, headers = {}, rawBody = "",
11466
11651
  if (level === "error" && !failed) return;
11467
11652
  const logDir = clickUpWebhookAuditLogDir();
11468
11653
  fs5.mkdirSync(logDir, { recursive: true });
11469
- const secretValues = [resolveSecretReference(config?.apiToken), state?.secret, config?.webhook?.secret].filter(Boolean);
11654
+ const secretValues = [
11655
+ resolveSecretReference(config?.apiToken),
11656
+ resolveSecretReference(config?.github?.apiToken),
11657
+ state?.secret,
11658
+ config?.webhook?.secret,
11659
+ resolveSecretReference(config?.github?.webhook?.secret)
11660
+ ].filter(Boolean);
11470
11661
  let requestFile;
11471
11662
  if (level === "verbose") {
11472
11663
  const absoluteRequestFile = clickUpWebhookRequestFilePath(at, logDir);
@@ -11928,6 +12119,176 @@ function runClickUpWebhookRouteInBackground({ payload, config, state, worktree,
11928
12119
  });
11929
12120
  return route;
11930
12121
  }
12122
+ async function routeGitHubWebhookEventUnlocked({ payload, event = "", delivery = "", config, state = {}, worktree = process.cwd(), clickupClient, githubClient = null, openCodeClient, sendSessionEvent = sendOpenCodeSessionEvent, verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery } = {}) {
12123
+ const normalizedEvent = String(event || payload?.event || "").trim();
12124
+ const action = String(payload?.action || "").trim();
12125
+ const eventKey = gitHubWebhookEventKey({ headers: { "x-github-delivery": delivery, "x-github-event": normalizedEvent }, event: normalizedEvent, payload });
12126
+ if (!config?.github?.webhook?.events?.includes(normalizedEvent)) {
12127
+ return { ok: true, action: "ignored", reason: "unsupported_event", event: normalizedEvent, eventKey };
12128
+ }
12129
+ let pr;
12130
+ try {
12131
+ pr = await gitHubPullRequestContextFromPayload(payload, normalizedEvent, githubClient);
12132
+ } catch (error) {
12133
+ appendClickUpWebhookLocalLog(worktree, { type: "github_pr_context_failed", event: normalizedEvent, delivery, message: error.message });
12134
+ return { ok: false, action: "error", reason: "pull_request_context_failed", event: normalizedEvent, eventKey, message: error.message };
12135
+ }
12136
+ if (!pr) {
12137
+ appendClickUpWebhookLocalLog(worktree, { type: "github_pr_context_unavailable", event: normalizedEvent, delivery, action });
12138
+ return { ok: true, action: "ignored", reason: "pull_request_context_unavailable", event: normalizedEvent, eventKey };
12139
+ }
12140
+ const taskId = String(pr.taskId || "").trim();
12141
+ if (!taskId) {
12142
+ appendClickUpWebhookLocalLog(worktree, { type: "github_task_id_unavailable", event: normalizedEvent, delivery, prNumber: pr.number || null, headRef: pr.headRef || null });
12143
+ return { ok: false, action: "error", reason: "task_id_unavailable", event: normalizedEvent, eventKey, prNumber: pr.number || null };
12144
+ }
12145
+ const task = payload.task || (clickupClient?.getTask ? await clickupClient.getTask(taskId) : null);
12146
+ if (!task) {
12147
+ appendClickUpWebhookLocalLog(worktree, { type: "github_clickup_task_unavailable", taskId, event: normalizedEvent, delivery, prNumber: pr.number || null });
12148
+ return { ok: false, action: "error", reason: "task_unavailable", taskId, event: normalizedEvent, eventKey, prNumber: pr.number || null };
12149
+ }
12150
+ if (isClickUpTaskTerminal(task, {}, config.routing?.ignoredStatuses)) {
12151
+ return { ok: true, action: "ignored", reason: "terminal_status", taskId, event: normalizedEvent, eventKey, prNumber: pr.number || null };
12152
+ }
12153
+ const existingMetadata = clickUpTaskAgentMetadata(task, config.routing.metadataFieldId);
12154
+ const metadata = normalizeAgentMetadataJson(existingMetadata);
12155
+ const rawSessionId = getNestedMetadataValue(metadata, config.routing.metadataKey) || getNestedMetadataValue(metadata, "optima.sessions.product_manager");
12156
+ const sessionId = typeof rawSessionId === "string" && rawSessionId.startsWith("ses_") ? rawSessionId : "";
12157
+ const directory = String(
12158
+ getNestedMetadataValue(metadata, "task.worktree") || getNestedMetadataValue(metadata, "task.subtask_worktree") || ""
12159
+ ).trim();
12160
+ const branch = String(
12161
+ getNestedMetadataValue(metadata, "task.branch") || getNestedMetadataValue(metadata, "task.subtask_branch") || pr.headRef || ""
12162
+ ).trim();
12163
+ if (!sessionId) {
12164
+ appendClickUpWebhookLocalLog(worktree, { type: "github_session_missing", taskId, event: normalizedEvent, delivery, prNumber: pr.number || null, branch: branch || null });
12165
+ return { ok: false, action: "error", reason: "session_missing", taskId, event: normalizedEvent, eventKey, prNumber: pr.number || null, branch: branch || null };
12166
+ }
12167
+ if (!directory) {
12168
+ appendClickUpWebhookLocalLog(worktree, { type: "github_worktree_missing", taskId, event: normalizedEvent, delivery, sessionId, prNumber: pr.number || null });
12169
+ return { ok: false, action: "error", reason: "worktree_missing", taskId, event: normalizedEvent, eventKey, sessionId, prNumber: pr.number || null, branch: branch || null };
12170
+ }
12171
+ const prompt = formatGitHubWebhookPrompt({ event: normalizedEvent, action, pr, payload, taskId });
12172
+ const nextMetadata = mergeGitHubWebhookMetadata(existingMetadata, { pr, event: normalizedEvent, action, delivery });
12173
+ if (typeof clickupClient?.updateTaskMetadata === "function") {
12174
+ await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: nextMetadata });
12175
+ }
12176
+ const deliveryResult = await deliverClickUpSessionEventWithVerification({
12177
+ openCodeClient,
12178
+ sendSessionEvent,
12179
+ clickupClient,
12180
+ worktree,
12181
+ taskId,
12182
+ sessionId,
12183
+ agent: config.routing.targetAgent,
12184
+ text: prompt,
12185
+ directory,
12186
+ opencodeBaseUrl: config.opencode?.baseUrl,
12187
+ directPrompt: config.opencode?.promptDelivery === "http",
12188
+ directDelivery: "steer",
12189
+ acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true,
12190
+ eventMarkers: [taskId, String(pr.number || ""), normalizedEvent].filter(Boolean),
12191
+ verifySessionEventDelivery,
12192
+ applyBlockerOnFailure: false
12193
+ });
12194
+ if (!deliveryResult.ok) {
12195
+ appendClickUpWebhookLocalLog(worktree, { type: "github_message_delivery_failed", taskId, event: normalizedEvent, delivery, sessionId, prNumber: pr.number || null, reason: deliveryResult.reason || "message_delivery_failed" });
12196
+ return { ...deliveryResult, eventKey, taskId, event: normalizedEvent, prNumber: pr.number || null, branch: branch || pr.headRef || null, worktree: directory };
12197
+ }
12198
+ return {
12199
+ ok: true,
12200
+ action: "github_event_delivered",
12201
+ taskId,
12202
+ sessionId,
12203
+ event: normalizedEvent,
12204
+ eventKey,
12205
+ prNumber: pr.number || null,
12206
+ branch: branch || pr.headRef || null,
12207
+ worktree: directory,
12208
+ deliveryVerification: deliveryResult.verification,
12209
+ deliveryAdmission: deliveryResult.admissionVerification,
12210
+ deliveryFallback: deliveryResult.fallback
12211
+ };
12212
+ }
12213
+ async function routeGitHubWebhookEvent(options = {}) {
12214
+ let taskId = "";
12215
+ try {
12216
+ const pr = await gitHubPullRequestContextFromPayload(options.payload || {}, options.event || "", options.githubClient || null);
12217
+ taskId = pr?.taskId || "";
12218
+ } catch {
12219
+ taskId = "";
12220
+ }
12221
+ return withClickUpTaskRouteLock(taskId, () => routeGitHubWebhookEventUnlocked(options));
12222
+ }
12223
+ function runGitHubWebhookRouteInBackground({ payload, event, delivery, config, state, worktree, clickupClient, githubClient, openCodeClient, sendSessionEvent, verifySessionEventDelivery } = {}) {
12224
+ const eventKey = gitHubWebhookEventKey({ headers: { "x-github-delivery": delivery, "x-github-event": event }, event, payload });
12225
+ appendClickUpWebhookLocalLog(worktree, { type: "github_webhook_route_started", event, eventKey, async: true, delivery: delivery || null });
12226
+ const route = Promise.resolve().then(() => routeGitHubWebhookEvent({ payload, event, delivery, config, state, worktree, clickupClient, githubClient, openCodeClient, sendSessionEvent, verifySessionEventDelivery })).then((result) => {
12227
+ appendClickUpWebhookLocalLog(worktree, { type: "github_webhook_route_finished", async: true, ...summarizeGitHubRouteForLog(result, payload, event) });
12228
+ return result;
12229
+ }).catch((error) => {
12230
+ appendClickUpWebhookLocalLog(worktree, { type: "github_webhook_route_failed", event, eventKey, async: true, delivery: delivery || null, message: error.message });
12231
+ return { ok: false, action: "error", reason: "background_route_failed", message: error.message };
12232
+ });
12233
+ route.catch(() => {
12234
+ });
12235
+ return route;
12236
+ }
12237
+ async function handleGitHubWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, githubClient = null, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date(), sendSessionEvent, verifySessionEventDelivery, asyncRouting = false } = {}) {
12238
+ let payload = null;
12239
+ let handled = null;
12240
+ let authenticatedWebhook = false;
12241
+ let receivedAt = null;
12242
+ let latestPersistedState = state;
12243
+ const persistState = (nextState) => {
12244
+ latestPersistedState = nextState;
12245
+ if (saveState) saveState(nextState);
12246
+ };
12247
+ const finish = (result) => {
12248
+ handled = result;
12249
+ receivedAt ??= now();
12250
+ const auditState = authenticatedWebhook ? { ...latestPersistedState, githubLastWebhookAt: receivedAt.toISOString() } : latestPersistedState;
12251
+ if (saveState && authenticatedWebhook) persistState(auditState);
12252
+ writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state: auditState, handled, payload, at: receivedAt });
12253
+ return result;
12254
+ };
12255
+ try {
12256
+ if (method !== "POST") return finish({ ok: false, status: 405, reason: "method_not_allowed" });
12257
+ if (url !== null) {
12258
+ const requestPath = new URL(String(url), config?.github?.webhook?.publicUrl || config?.webhook?.publicUrl || "http://localhost").pathname;
12259
+ if (requestPath !== gitHubWebhookExpectedPath(config)) return finish({ ok: false, status: 404, reason: "wrong_path" });
12260
+ }
12261
+ if (!gitHubWebhookEnabled(config)) return finish({ ok: false, status: 404, reason: "github_webhook_disabled" });
12262
+ const maxBodyBytes = Number(config?.webhook?.maxBodyBytes || CLICKUP_WEBHOOK_MAX_BODY_BYTES);
12263
+ const bodyBytes = Buffer.isBuffer(rawBody) ? rawBody.length : Buffer.byteLength(String(rawBody || ""));
12264
+ if (bodyBytes > maxBodyBytes) return finish({ ok: false, status: 413, reason: "body_too_large" });
12265
+ const signature = headers["x-hub-signature-256"] || headers["X-Hub-Signature-256"];
12266
+ const secret = resolveSecretReference(config.github.webhook.secret);
12267
+ if (!verifyGitHubSignature(rawBody, signature, secret)) return finish({ ok: false, status: 401, reason: "invalid_signature" });
12268
+ authenticatedWebhook = true;
12269
+ receivedAt = now();
12270
+ try {
12271
+ payload = JSON.parse(Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : String(rawBody));
12272
+ } catch {
12273
+ return finish({ ok: false, status: 400, reason: "invalid_json" });
12274
+ }
12275
+ const event = String(headers["x-github-event"] || headers["X-GitHub-Event"] || "").trim();
12276
+ const delivery = String(headers["x-github-delivery"] || headers["X-GitHub-Delivery"] || "").trim();
12277
+ const eventKey = gitHubWebhookEventKey({ headers, event, payload });
12278
+ const remembered = rememberGitHubWebhookEvent({ ...state, githubLastWebhookAt: receivedAt.toISOString() }, eventKey);
12279
+ persistState(remembered.state);
12280
+ if (remembered.duplicate) return finish({ ok: true, status: 200, reason: "duplicate_event", result: { action: "ignored", reason: "duplicate", event, eventKey } });
12281
+ if (asyncRouting) {
12282
+ runGitHubWebhookRouteInBackground({ payload, event, delivery, config, state: remembered.state, worktree, clickupClient, githubClient, openCodeClient, sendSessionEvent, verifySessionEventDelivery });
12283
+ return finish({ ok: true, status: 200, reason: "accepted", result: { action: "accepted", event, eventKey } });
12284
+ }
12285
+ const result = await routeGitHubWebhookEvent({ payload, event, delivery, config, state: remembered.state, worktree, clickupClient, githubClient, openCodeClient, sendSessionEvent, verifySessionEventDelivery });
12286
+ return finish({ ok: result.ok, status: result.ok ? 200 : 422, result });
12287
+ } catch (error) {
12288
+ writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state, handled, error, payload, at: now() });
12289
+ throw error;
12290
+ }
12291
+ }
11931
12292
  async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date(), sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, asyncRouting = false } = {}) {
11932
12293
  let payload = null;
11933
12294
  let handled = null;
@@ -11990,13 +12351,17 @@ function clickUpListenerFingerprint({ config, state, worktree, clickupClient, op
11990
12351
  teamId: config?.teamId || "",
11991
12352
  publicUrl: config?.webhook?.publicUrl || "",
11992
12353
  path: clickUpWebhookExpectedPath(config),
12354
+ githubPublicUrl: config?.github?.webhook?.publicUrl || "",
12355
+ githubPath: gitHubWebhookExpectedPath(config),
12356
+ githubEnabled: gitHubWebhookEnabled(config),
12357
+ githubEvents: config?.github?.webhook?.events || [],
11993
12358
  location: config?.webhook?.location || {},
11994
12359
  webhookId: state?.webhookId || "",
11995
12360
  secret: state?.secret || "",
11996
12361
  events: config?.webhook?.events || []
11997
12362
  });
11998
12363
  }
11999
- function startClickUpWebhookListener({ config, state, worktree, clickupClient, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
12364
+ function startClickUpWebhookListener({ config, state, worktree, clickupClient, githubClient = null, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
12000
12365
  if (!isClickUpWebhookStateActive(state, config)) return { active: false, ready: false, reason: "inactive_state" };
12001
12366
  const key = clickUpListenerKey(config);
12002
12367
  const fingerprint = clickUpListenerFingerprint({ config, state, worktree, clickupClient, openCodeClient });
@@ -12015,7 +12380,9 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
12015
12380
  return;
12016
12381
  }
12017
12382
  const requestPath = new URL(req.url || "/", config.webhook.publicUrl).pathname;
12018
- if (requestPath !== clickUpWebhookExpectedPath(config)) {
12383
+ const isClickUpPath = requestPath === clickUpWebhookExpectedPath(config);
12384
+ const isGitHubPath = gitHubWebhookEnabled(config) && requestPath === gitHubWebhookExpectedPath(config);
12385
+ if (!isClickUpPath && !isGitHubPath) {
12019
12386
  res.writeHead(404, { "Content-Type": "application/json" });
12020
12387
  res.end(JSON.stringify({ ok: false, reason: "wrong_path" }));
12021
12388
  return;
@@ -12042,7 +12409,7 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
12042
12409
  if (overflow || res.writableEnded) return;
12043
12410
  const latestState = readClickUpWebhookState(worktree, config);
12044
12411
  try {
12045
- const handled = await handleClickUpWebhookRequest({
12412
+ const common = {
12046
12413
  method: req.method,
12047
12414
  url: req.url,
12048
12415
  headers: req.headers,
@@ -12054,7 +12421,8 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
12054
12421
  openCodeClient,
12055
12422
  saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, config),
12056
12423
  asyncRouting: true
12057
- });
12424
+ };
12425
+ const handled = isGitHubPath ? await handleGitHubWebhookRequest({ ...common, githubClient }) : await handleClickUpWebhookRequest(common);
12058
12426
  res.writeHead(handled.status, { "Content-Type": "application/json" });
12059
12427
  res.end(JSON.stringify({ ok: handled.ok, reason: handled.reason, result: handled.result?.action }));
12060
12428
  } catch (error) {
@@ -13172,6 +13540,7 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
13172
13540
  const operatingTeamMode = getOperatingTeamMode(repoCfg);
13173
13541
  const clickUpWebhookValidation = normalizeClickUpWebhookConfig(pickConfiguredClickUp(input, repoCfg, resolvedPluginOptions), worktree);
13174
13542
  const runtimeClickUpClient = input.clickupClient || (clickUpWebhookValidation.config?.test === true ? createTestClickUpApiClient(clickUpWebhookValidation.config) : createClickUpApiClient(clickUpWebhookValidation.config, input.fetch));
13543
+ const runtimeGitHubClient = input.githubClient || (clickUpWebhookValidation.config?.github?.webhook?.enabled === true ? createGitHubApiClient(clickUpWebhookValidation.config.github, input.fetch) : null);
13175
13544
  let clickUpWebhookRuntime = { active: false, valid: false, reason: "not_configured" };
13176
13545
  if (clickUpWebhookValidation.complete) {
13177
13546
  try {
@@ -13196,6 +13565,7 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
13196
13565
  state: listenerState,
13197
13566
  worktree,
13198
13567
  clickupClient: lifecycleClickUpClient,
13568
+ githubClient: runtimeGitHubClient,
13199
13569
  openCodeClient: input.client,
13200
13570
  listenerRegistry
13201
13571
  });
@@ -13871,7 +14241,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
13871
14241
  }
13872
14242
  };
13873
14243
  }
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 };
14244
+ 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, createGitHubApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, gitHubPullRequestContextFromPayload, gitHubWebhookExpectedPath, handleClickUpWebhookRequest, handleGitHubWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, routeGitHubWebhookEvent, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, verifyGitHubSignature, 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 };
13875
14245
  export {
13876
14246
  OptimaPlugin as default
13877
14247
  };
@@ -8634,6 +8634,7 @@ var activeWorkflows = /* @__PURE__ */ new Map();
8634
8634
  var activeClickUpWebhookListeners = /* @__PURE__ */ new Map();
8635
8635
  var activeClickUpWebhookLifecycleRegistry = /* @__PURE__ */ new Map();
8636
8636
  var activeClickUpTaskRoutes = /* @__PURE__ */ new Map();
8637
+ var GITHUB_WEBHOOK_EVENTS = ["pull_request", "pull_request_review", "pull_request_review_comment", "issue_comment"];
8637
8638
  function isRootDirectory(candidate) {
8638
8639
  const resolved = path6.resolve(candidate);
8639
8640
  return resolved === path6.parse(resolved).root;
@@ -9887,9 +9888,48 @@ function normalizeNonNegativeInteger(value, defaultValue) {
9887
9888
  const number = Number(value);
9888
9889
  return Number.isInteger(number) && number >= 0 ? number : defaultValue;
9889
9890
  }
9891
+ function defaultGitHubWebhookPublicUrl(clickupPublicUrl = "") {
9892
+ const raw = String(clickupPublicUrl || "").trim();
9893
+ if (!raw) return "";
9894
+ try {
9895
+ const url = new URL(raw);
9896
+ url.pathname = "/optima/github/webhook";
9897
+ url.search = "";
9898
+ url.hash = "";
9899
+ return url.toString();
9900
+ } catch {
9901
+ return raw.replace(/\/optima\/clickup\/webhook\b/, "/optima/github/webhook");
9902
+ }
9903
+ }
9904
+ function normalizeGitHubWebhookConfig(rawGithub = {}, clickupWebhook = {}) {
9905
+ const github = isPlainObject(rawGithub) ? rawGithub : {};
9906
+ const webhook = isPlainObject(github.webhook) ? github.webhook : {};
9907
+ const enabled = github.enabled === true || webhook.enabled === true;
9908
+ const events = Array.isArray(webhook.events) && webhook.events.length > 0 ? [...new Set(webhook.events.map((event) => String(event || "").trim()).filter(Boolean))] : [...GITHUB_WEBHOOK_EVENTS];
9909
+ const publicUrl = String(webhook.public_url || webhook.publicUrl || github.public_url || github.publicUrl || defaultGitHubWebhookPublicUrl(clickupWebhook.publicUrl)).trim();
9910
+ const secret = String(webhook.secret || github.secret || "").trim();
9911
+ const apiToken = String(github.api_token || github.apiToken || "").trim();
9912
+ const repository = String(github.repository || github.repo || "").trim();
9913
+ const [ownerFromRepo, nameFromRepo] = repository.includes("/") ? repository.split("/", 2) : ["", ""];
9914
+ return {
9915
+ enabled,
9916
+ apiToken,
9917
+ repository,
9918
+ owner: String(github.owner || ownerFromRepo || "").trim(),
9919
+ repo: String(github.repo_name || github.repoName || nameFromRepo || "").trim(),
9920
+ webhook: {
9921
+ enabled,
9922
+ publicUrl,
9923
+ secret,
9924
+ path: publicUrl ? new URL(publicUrl, "http://localhost").pathname : "/optima/github/webhook",
9925
+ events
9926
+ }
9927
+ };
9928
+ }
9890
9929
  function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd()) {
9891
9930
  const raw = isPlainObject(rawClickUp) ? rawClickUp : {};
9892
9931
  const webhook = isPlainObject(raw.webhook) ? raw.webhook : {};
9932
+ const github = normalizeGitHubWebhookConfig(raw.github, { publicUrl: String(webhook.public_url || webhook.publicUrl || "").trim() });
9893
9933
  const routing = isPlainObject(raw.routing) ? raw.routing : {};
9894
9934
  const opencode = isPlainObject(raw.opencode) ? raw.opencode : {};
9895
9935
  const openchamber = isPlainObject(raw.openchamber) ? raw.openchamber : {};
@@ -9942,6 +9982,7 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9942
9982
  },
9943
9983
  events
9944
9984
  },
9985
+ github,
9945
9986
  routing: {
9946
9987
  targetAgent: String(routing.target_agent || routing.targetAgent || "workflow_product_manager").trim(),
9947
9988
  productManagerAssigneeId: String(routing.product_manager_assignee_id || routing.productManagerAssigneeId || "").trim(),
@@ -9963,6 +10004,13 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9963
10004
  if (!config.webhook.bindHost) errors.push("clickup.webhook.bind_host is required");
9964
10005
  if (!Number.isInteger(config.webhook.bindPort) || config.webhook.bindPort <= 0) errors.push("clickup.webhook.bind_port must be a positive integer");
9965
10006
  if (!config.webhook.location.list_id && !config.webhook.location.folder_id && !config.webhook.location.space_id) errors.push("one clickup.webhook.location id is required");
10007
+ if (config.github.webhook.enabled) {
10008
+ if (!config.github.webhook.publicUrl) errors.push("clickup.github.webhook.public_url is required when GitHub webhook is enabled");
10009
+ if (!config.github.webhook.secret) errors.push("clickup.github.webhook.secret is required when GitHub webhook is enabled");
10010
+ if (!config.github.webhook.path || config.github.webhook.path === clickUpWebhookExpectedPath(config)) errors.push("clickup.github.webhook.path must be distinct from the ClickUp webhook path");
10011
+ const missingGitHubEvents = GITHUB_WEBHOOK_EVENTS.filter((event) => !config.github.webhook.events.includes(event));
10012
+ if (missingGitHubEvents.length > 0) errors.push(`clickup.github.webhook.events missing: ${missingGitHubEvents.join(", ")}`);
10013
+ }
9966
10014
  if (config.opencode.baseUrl) {
9967
10015
  try {
9968
10016
  new URL(config.opencode.baseUrl);
@@ -10000,6 +10048,8 @@ function sanitizeClickUpWebhookState(state = {}, config = null) {
10000
10048
  listener: isPlainObject(state.listener) ? state.listener : {},
10001
10049
  pendingSessions: isPlainObject(state.pendingSessions) ? state.pendingSessions : {},
10002
10050
  lastWebhookAt: state.lastWebhookAt || state.last_webhook_at || null,
10051
+ githubLastWebhookAt: state.githubLastWebhookAt || state.github_last_webhook_at || null,
10052
+ githubRecentEventKeys: Array.isArray(state.githubRecentEventKeys) ? state.githubRecentEventKeys.slice(-200) : [],
10003
10053
  recentEventKeys
10004
10054
  };
10005
10055
  }
@@ -10116,6 +10166,29 @@ function createClickUpApiClient(config, fetchImpl = globalThis.fetch) {
10116
10166
  }
10117
10167
  };
10118
10168
  }
10169
+ function createGitHubApiClient(config = {}, fetchImpl = globalThis.fetch) {
10170
+ const token = resolveSecretReference(config?.apiToken);
10171
+ const owner = String(config?.owner || "").trim();
10172
+ const repo = String(config?.repo || "").trim();
10173
+ const request = async (pathname) => {
10174
+ if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable; inject a GitHub client for live PR lookup");
10175
+ if (!owner || !repo) throw new Error("GitHub repository owner/repo is not configured");
10176
+ const response = await fetchImpl(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}${pathname}`, {
10177
+ headers: {
10178
+ Accept: "application/vnd.github+json",
10179
+ "X-GitHub-Api-Version": "2022-11-28",
10180
+ ...token ? { Authorization: `Bearer ${token}` } : {}
10181
+ }
10182
+ });
10183
+ if (!response.ok) throw new Error(`GitHub API request failed: ${response.status}`);
10184
+ return response.json();
10185
+ };
10186
+ return {
10187
+ async getPullRequest(number) {
10188
+ return request(`/pulls/${encodeURIComponent(number)}`);
10189
+ }
10190
+ };
10191
+ }
10119
10192
  function createTestClickUpApiClient(config) {
10120
10193
  const metadata = /* @__PURE__ */ new Map();
10121
10194
  return {
@@ -10346,6 +10419,19 @@ function verifyClickUpSignature(rawBody, signatureHeader, secret) {
10346
10419
  const wanted = Buffer.from(expected, "hex");
10347
10420
  return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
10348
10421
  }
10422
+ function verifyGitHubSignature(rawBody, signatureHeader, secret) {
10423
+ return verifyClickUpSignature(rawBody, signatureHeader, secret);
10424
+ }
10425
+ function gitHubWebhookEventKey({ headers = {}, event = "", payload = {} } = {}) {
10426
+ const delivery = headers["x-github-delivery"] || headers["X-GitHub-Delivery"] || payload.delivery || payload.delivery_id || "";
10427
+ const id = delivery || payload.pull_request?.id || payload.comment?.id || payload.review?.id || payload.issue?.id || "unknown";
10428
+ return ["github", event || headers["x-github-event"] || headers["X-GitHub-Event"] || "event", id].filter(Boolean).join(":");
10429
+ }
10430
+ function rememberGitHubWebhookEvent(state = {}, eventKey, limit = 200) {
10431
+ const recent = Array.isArray(state.githubRecentEventKeys) ? state.githubRecentEventKeys : [];
10432
+ if (recent.includes(eventKey)) return { duplicate: true, state };
10433
+ return { duplicate: false, state: { ...state, githubRecentEventKeys: [...recent, eventKey].slice(-limit) } };
10434
+ }
10349
10435
  async function resyncClickUpWebhookForSignatureDrift({ rawBody, signature, config, state = {}, worktree = process.cwd(), clickupClient, saveState } = {}) {
10350
10436
  const validation = clickUpWebhookValidationFromConfig(config);
10351
10437
  if (!clickupClient?.listWebhooks && !clickupClient?.createWebhook) {
@@ -10552,6 +10638,105 @@ function clickUpTaskAgentMetadata(task = {}, fieldId = "") {
10552
10638
  const field = fields.find((item) => String(item?.id || "") === fieldId || String(item?.name || "") === "agent_metadata");
10553
10639
  return field?.value || "";
10554
10640
  }
10641
+ function gitHubWebhookExpectedPath(config = {}) {
10642
+ return config?.github?.webhook?.path || "/optima/github/webhook";
10643
+ }
10644
+ function gitHubWebhookEnabled(config = {}) {
10645
+ return Boolean(config?.github?.webhook?.enabled && resolveSecretReference(config?.github?.webhook?.secret));
10646
+ }
10647
+ function clickUpTaskIdFromGitHubBranch(branch = "") {
10648
+ const tail = String(branch || "").trim().split("/").filter(Boolean).at(-1) || "";
10649
+ if (!tail) return "";
10650
+ const subtask = tail.match(/-subtask-(.+)$/);
10651
+ return branchSafeClickUpId(subtask ? subtask[1] : tail);
10652
+ }
10653
+ async function gitHubPullRequestContextFromPayload(payload = {}, event = "", githubClient = null) {
10654
+ let pr = payload.pull_request || null;
10655
+ if (!pr && event === "issue_comment" && payload.issue?.pull_request && githubClient?.getPullRequest) {
10656
+ pr = await githubClient.getPullRequest(payload.issue.number);
10657
+ }
10658
+ if (!pr) return null;
10659
+ const headRef = String(pr.head?.ref || pr.head_ref || "").trim();
10660
+ const baseRef = String(pr.base?.ref || pr.base_ref || "").trim();
10661
+ return {
10662
+ number: pr.number || payload.issue?.number || "",
10663
+ id: pr.id || "",
10664
+ url: pr.html_url || pr.url || "",
10665
+ title: pr.title || payload.issue?.title || "",
10666
+ state: pr.state || "",
10667
+ merged: pr.merged === true,
10668
+ headRef,
10669
+ baseRef,
10670
+ taskId: clickUpTaskIdFromGitHubBranch(headRef),
10671
+ repository: pr.head?.repo?.full_name || pr.base?.repo?.full_name || payload.repository?.full_name || ""
10672
+ };
10673
+ }
10674
+ function gitHubWebhookCommentBody(payload = {}) {
10675
+ return String(payload.comment?.body || payload.review?.body || payload.pull_request?.body || "").trim();
10676
+ }
10677
+ function formatGitHubWebhookPrompt({ event = "", action = "", pr = {}, payload = {}, taskId = "" } = {}) {
10678
+ const sender = payload.sender?.login || payload.comment?.user?.login || payload.review?.user?.login || "unknown";
10679
+ const reviewState = String(payload.review?.state || "").trim();
10680
+ const commentBody = gitHubWebhookCommentBody(payload);
10681
+ const lines = [
10682
+ "[GitHub Webhook Event]",
10683
+ `Event: ${event}`,
10684
+ `Action: ${action || "unknown"}`,
10685
+ `Task ID: ${taskId || pr.taskId || "unknown"}`,
10686
+ `Pull Request: #${pr.number || "unknown"} ${pr.url || ""}`.trim(),
10687
+ `PR Branch: ${pr.headRef || "unknown"} -> ${pr.baseRef || "unknown"}`,
10688
+ `Sender: ${sender}`,
10689
+ reviewState ? `Review State: ${reviewState}` : null,
10690
+ commentBody ? ["", "Comment/Review Body:", commentBody].join("\n") : null,
10691
+ "",
10692
+ "Required handling:",
10693
+ "- Answer human GitHub comments in GitHub.",
10694
+ "- If the comment/review implies a change, reply first with what you will do, move the ClickUp task/subtask to in progress, implement/fix in the same branch, push, return ClickUp to validation, update the PR, and reply again with what changed.",
10695
+ "- If this is an approved parent PR by the configured final approver/CTO, continue the merge-to-dev, Vercel preproduction verification, smoke regression, cleanup, and ClickUp completion workflow.",
10696
+ "- Keep Optima runtime/process failures out of ClickUp comments; use local logs for process noise."
10697
+ ].filter(Boolean);
10698
+ return lines.join("\n");
10699
+ }
10700
+ function mergeGitHubWebhookMetadata(existingMetadata, { pr = {}, event = "", action = "", delivery = "" } = {}) {
10701
+ const current = normalizeAgentMetadataJson(existingMetadata);
10702
+ const next = {
10703
+ ...current,
10704
+ task: {
10705
+ ...isPlainObject(current.task) ? current.task : {},
10706
+ github: {
10707
+ ...isPlainObject(current.task?.github) ? current.task.github : {},
10708
+ pull_request: Object.fromEntries(Object.entries({
10709
+ number: pr.number || void 0,
10710
+ url: pr.url || void 0,
10711
+ source_branch: pr.headRef || void 0,
10712
+ target_branch: pr.baseRef || void 0,
10713
+ repository: pr.repository || void 0
10714
+ }).filter(([, value]) => value !== void 0 && value !== "")),
10715
+ last_webhook: {
10716
+ event,
10717
+ action,
10718
+ delivery,
10719
+ at: (/* @__PURE__ */ new Date()).toISOString()
10720
+ }
10721
+ }
10722
+ }
10723
+ };
10724
+ return JSON.stringify(sortJsonValue(next), null, 2);
10725
+ }
10726
+ function summarizeGitHubRouteForLog(result = {}, payload = {}, event = "") {
10727
+ return {
10728
+ taskId: result.taskId || null,
10729
+ event: event || null,
10730
+ eventKey: result.eventKey || null,
10731
+ ok: result.ok === true,
10732
+ action: result.action || null,
10733
+ reason: result.reason || null,
10734
+ sessionId: result.sessionId || null,
10735
+ pullRequest: result.prNumber || payload?.pull_request?.number || payload?.issue?.number || null,
10736
+ branch: result.branch || null,
10737
+ worktree: result.worktree || null
10738
+ };
10739
+ }
10555
10740
  function getNestedMetadataValue(metadata, dottedKey) {
10556
10741
  const parts = String(dottedKey || "").split(".").filter(Boolean);
10557
10742
  let cursor = metadata;
@@ -11457,9 +11642,9 @@ function buildClickUpWebhookAuditSummary({ method, url, config, handled, error,
11457
11642
  ok: Boolean(handled?.ok),
11458
11643
  reason: handled?.reason || result.reason || (error ? "handler_error" : void 0),
11459
11644
  action: result.action,
11460
- event: payload ? clickUpEventType(payload) : void 0,
11645
+ event: result.event || (payload ? clickUpEventType(payload) : void 0),
11461
11646
  task: result.taskId || clickUpTaskIdFromPayload(payload || {}),
11462
- comment: clickUpCommentFromPayload(payload || {})?.id || payload?.comment_id || payload?.commentId,
11647
+ comment: clickUpCommentFromPayload(payload || {})?.id || payload?.comment_id || payload?.commentId || payload?.comment?.id || payload?.review?.id,
11463
11648
  session: result.sessionId,
11464
11649
  base_path: config?.basePath || "",
11465
11650
  request_file: requestFile || void 0
@@ -11473,7 +11658,13 @@ function writeClickUpWebhookAuditLog({ method, url, headers = {}, rawBody = "",
11473
11658
  if (level === "error" && !failed) return;
11474
11659
  const logDir = clickUpWebhookAuditLogDir();
11475
11660
  fs5.mkdirSync(logDir, { recursive: true });
11476
- const secretValues = [resolveSecretReference(config?.apiToken), state?.secret, config?.webhook?.secret].filter(Boolean);
11661
+ const secretValues = [
11662
+ resolveSecretReference(config?.apiToken),
11663
+ resolveSecretReference(config?.github?.apiToken),
11664
+ state?.secret,
11665
+ config?.webhook?.secret,
11666
+ resolveSecretReference(config?.github?.webhook?.secret)
11667
+ ].filter(Boolean);
11477
11668
  let requestFile;
11478
11669
  if (level === "verbose") {
11479
11670
  const absoluteRequestFile = clickUpWebhookRequestFilePath(at, logDir);
@@ -11935,6 +12126,176 @@ function runClickUpWebhookRouteInBackground({ payload, config, state, worktree,
11935
12126
  });
11936
12127
  return route;
11937
12128
  }
12129
+ async function routeGitHubWebhookEventUnlocked({ payload, event = "", delivery = "", config, state = {}, worktree = process.cwd(), clickupClient, githubClient = null, openCodeClient, sendSessionEvent = sendOpenCodeSessionEvent, verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery } = {}) {
12130
+ const normalizedEvent = String(event || payload?.event || "").trim();
12131
+ const action = String(payload?.action || "").trim();
12132
+ const eventKey = gitHubWebhookEventKey({ headers: { "x-github-delivery": delivery, "x-github-event": normalizedEvent }, event: normalizedEvent, payload });
12133
+ if (!config?.github?.webhook?.events?.includes(normalizedEvent)) {
12134
+ return { ok: true, action: "ignored", reason: "unsupported_event", event: normalizedEvent, eventKey };
12135
+ }
12136
+ let pr;
12137
+ try {
12138
+ pr = await gitHubPullRequestContextFromPayload(payload, normalizedEvent, githubClient);
12139
+ } catch (error) {
12140
+ appendClickUpWebhookLocalLog(worktree, { type: "github_pr_context_failed", event: normalizedEvent, delivery, message: error.message });
12141
+ return { ok: false, action: "error", reason: "pull_request_context_failed", event: normalizedEvent, eventKey, message: error.message };
12142
+ }
12143
+ if (!pr) {
12144
+ appendClickUpWebhookLocalLog(worktree, { type: "github_pr_context_unavailable", event: normalizedEvent, delivery, action });
12145
+ return { ok: true, action: "ignored", reason: "pull_request_context_unavailable", event: normalizedEvent, eventKey };
12146
+ }
12147
+ const taskId = String(pr.taskId || "").trim();
12148
+ if (!taskId) {
12149
+ appendClickUpWebhookLocalLog(worktree, { type: "github_task_id_unavailable", event: normalizedEvent, delivery, prNumber: pr.number || null, headRef: pr.headRef || null });
12150
+ return { ok: false, action: "error", reason: "task_id_unavailable", event: normalizedEvent, eventKey, prNumber: pr.number || null };
12151
+ }
12152
+ const task = payload.task || (clickupClient?.getTask ? await clickupClient.getTask(taskId) : null);
12153
+ if (!task) {
12154
+ appendClickUpWebhookLocalLog(worktree, { type: "github_clickup_task_unavailable", taskId, event: normalizedEvent, delivery, prNumber: pr.number || null });
12155
+ return { ok: false, action: "error", reason: "task_unavailable", taskId, event: normalizedEvent, eventKey, prNumber: pr.number || null };
12156
+ }
12157
+ if (isClickUpTaskTerminal(task, {}, config.routing?.ignoredStatuses)) {
12158
+ return { ok: true, action: "ignored", reason: "terminal_status", taskId, event: normalizedEvent, eventKey, prNumber: pr.number || null };
12159
+ }
12160
+ const existingMetadata = clickUpTaskAgentMetadata(task, config.routing.metadataFieldId);
12161
+ const metadata = normalizeAgentMetadataJson(existingMetadata);
12162
+ const rawSessionId = getNestedMetadataValue(metadata, config.routing.metadataKey) || getNestedMetadataValue(metadata, "optima.sessions.product_manager");
12163
+ const sessionId = typeof rawSessionId === "string" && rawSessionId.startsWith("ses_") ? rawSessionId : "";
12164
+ const directory = String(
12165
+ getNestedMetadataValue(metadata, "task.worktree") || getNestedMetadataValue(metadata, "task.subtask_worktree") || ""
12166
+ ).trim();
12167
+ const branch = String(
12168
+ getNestedMetadataValue(metadata, "task.branch") || getNestedMetadataValue(metadata, "task.subtask_branch") || pr.headRef || ""
12169
+ ).trim();
12170
+ if (!sessionId) {
12171
+ appendClickUpWebhookLocalLog(worktree, { type: "github_session_missing", taskId, event: normalizedEvent, delivery, prNumber: pr.number || null, branch: branch || null });
12172
+ return { ok: false, action: "error", reason: "session_missing", taskId, event: normalizedEvent, eventKey, prNumber: pr.number || null, branch: branch || null };
12173
+ }
12174
+ if (!directory) {
12175
+ appendClickUpWebhookLocalLog(worktree, { type: "github_worktree_missing", taskId, event: normalizedEvent, delivery, sessionId, prNumber: pr.number || null });
12176
+ return { ok: false, action: "error", reason: "worktree_missing", taskId, event: normalizedEvent, eventKey, sessionId, prNumber: pr.number || null, branch: branch || null };
12177
+ }
12178
+ const prompt = formatGitHubWebhookPrompt({ event: normalizedEvent, action, pr, payload, taskId });
12179
+ const nextMetadata = mergeGitHubWebhookMetadata(existingMetadata, { pr, event: normalizedEvent, action, delivery });
12180
+ if (typeof clickupClient?.updateTaskMetadata === "function") {
12181
+ await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: nextMetadata });
12182
+ }
12183
+ const deliveryResult = await deliverClickUpSessionEventWithVerification({
12184
+ openCodeClient,
12185
+ sendSessionEvent,
12186
+ clickupClient,
12187
+ worktree,
12188
+ taskId,
12189
+ sessionId,
12190
+ agent: config.routing.targetAgent,
12191
+ text: prompt,
12192
+ directory,
12193
+ opencodeBaseUrl: config.opencode?.baseUrl,
12194
+ directPrompt: config.opencode?.promptDelivery === "http",
12195
+ directDelivery: "steer",
12196
+ acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true,
12197
+ eventMarkers: [taskId, String(pr.number || ""), normalizedEvent].filter(Boolean),
12198
+ verifySessionEventDelivery,
12199
+ applyBlockerOnFailure: false
12200
+ });
12201
+ if (!deliveryResult.ok) {
12202
+ appendClickUpWebhookLocalLog(worktree, { type: "github_message_delivery_failed", taskId, event: normalizedEvent, delivery, sessionId, prNumber: pr.number || null, reason: deliveryResult.reason || "message_delivery_failed" });
12203
+ return { ...deliveryResult, eventKey, taskId, event: normalizedEvent, prNumber: pr.number || null, branch: branch || pr.headRef || null, worktree: directory };
12204
+ }
12205
+ return {
12206
+ ok: true,
12207
+ action: "github_event_delivered",
12208
+ taskId,
12209
+ sessionId,
12210
+ event: normalizedEvent,
12211
+ eventKey,
12212
+ prNumber: pr.number || null,
12213
+ branch: branch || pr.headRef || null,
12214
+ worktree: directory,
12215
+ deliveryVerification: deliveryResult.verification,
12216
+ deliveryAdmission: deliveryResult.admissionVerification,
12217
+ deliveryFallback: deliveryResult.fallback
12218
+ };
12219
+ }
12220
+ async function routeGitHubWebhookEvent(options = {}) {
12221
+ let taskId = "";
12222
+ try {
12223
+ const pr = await gitHubPullRequestContextFromPayload(options.payload || {}, options.event || "", options.githubClient || null);
12224
+ taskId = pr?.taskId || "";
12225
+ } catch {
12226
+ taskId = "";
12227
+ }
12228
+ return withClickUpTaskRouteLock(taskId, () => routeGitHubWebhookEventUnlocked(options));
12229
+ }
12230
+ function runGitHubWebhookRouteInBackground({ payload, event, delivery, config, state, worktree, clickupClient, githubClient, openCodeClient, sendSessionEvent, verifySessionEventDelivery } = {}) {
12231
+ const eventKey = gitHubWebhookEventKey({ headers: { "x-github-delivery": delivery, "x-github-event": event }, event, payload });
12232
+ appendClickUpWebhookLocalLog(worktree, { type: "github_webhook_route_started", event, eventKey, async: true, delivery: delivery || null });
12233
+ const route = Promise.resolve().then(() => routeGitHubWebhookEvent({ payload, event, delivery, config, state, worktree, clickupClient, githubClient, openCodeClient, sendSessionEvent, verifySessionEventDelivery })).then((result) => {
12234
+ appendClickUpWebhookLocalLog(worktree, { type: "github_webhook_route_finished", async: true, ...summarizeGitHubRouteForLog(result, payload, event) });
12235
+ return result;
12236
+ }).catch((error) => {
12237
+ appendClickUpWebhookLocalLog(worktree, { type: "github_webhook_route_failed", event, eventKey, async: true, delivery: delivery || null, message: error.message });
12238
+ return { ok: false, action: "error", reason: "background_route_failed", message: error.message };
12239
+ });
12240
+ route.catch(() => {
12241
+ });
12242
+ return route;
12243
+ }
12244
+ async function handleGitHubWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, githubClient = null, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date(), sendSessionEvent, verifySessionEventDelivery, asyncRouting = false } = {}) {
12245
+ let payload = null;
12246
+ let handled = null;
12247
+ let authenticatedWebhook = false;
12248
+ let receivedAt = null;
12249
+ let latestPersistedState = state;
12250
+ const persistState = (nextState) => {
12251
+ latestPersistedState = nextState;
12252
+ if (saveState) saveState(nextState);
12253
+ };
12254
+ const finish = (result) => {
12255
+ handled = result;
12256
+ receivedAt ??= now();
12257
+ const auditState = authenticatedWebhook ? { ...latestPersistedState, githubLastWebhookAt: receivedAt.toISOString() } : latestPersistedState;
12258
+ if (saveState && authenticatedWebhook) persistState(auditState);
12259
+ writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state: auditState, handled, payload, at: receivedAt });
12260
+ return result;
12261
+ };
12262
+ try {
12263
+ if (method !== "POST") return finish({ ok: false, status: 405, reason: "method_not_allowed" });
12264
+ if (url !== null) {
12265
+ const requestPath = new URL(String(url), config?.github?.webhook?.publicUrl || config?.webhook?.publicUrl || "http://localhost").pathname;
12266
+ if (requestPath !== gitHubWebhookExpectedPath(config)) return finish({ ok: false, status: 404, reason: "wrong_path" });
12267
+ }
12268
+ if (!gitHubWebhookEnabled(config)) return finish({ ok: false, status: 404, reason: "github_webhook_disabled" });
12269
+ const maxBodyBytes = Number(config?.webhook?.maxBodyBytes || CLICKUP_WEBHOOK_MAX_BODY_BYTES);
12270
+ const bodyBytes = Buffer.isBuffer(rawBody) ? rawBody.length : Buffer.byteLength(String(rawBody || ""));
12271
+ if (bodyBytes > maxBodyBytes) return finish({ ok: false, status: 413, reason: "body_too_large" });
12272
+ const signature = headers["x-hub-signature-256"] || headers["X-Hub-Signature-256"];
12273
+ const secret = resolveSecretReference(config.github.webhook.secret);
12274
+ if (!verifyGitHubSignature(rawBody, signature, secret)) return finish({ ok: false, status: 401, reason: "invalid_signature" });
12275
+ authenticatedWebhook = true;
12276
+ receivedAt = now();
12277
+ try {
12278
+ payload = JSON.parse(Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : String(rawBody));
12279
+ } catch {
12280
+ return finish({ ok: false, status: 400, reason: "invalid_json" });
12281
+ }
12282
+ const event = String(headers["x-github-event"] || headers["X-GitHub-Event"] || "").trim();
12283
+ const delivery = String(headers["x-github-delivery"] || headers["X-GitHub-Delivery"] || "").trim();
12284
+ const eventKey = gitHubWebhookEventKey({ headers, event, payload });
12285
+ const remembered = rememberGitHubWebhookEvent({ ...state, githubLastWebhookAt: receivedAt.toISOString() }, eventKey);
12286
+ persistState(remembered.state);
12287
+ if (remembered.duplicate) return finish({ ok: true, status: 200, reason: "duplicate_event", result: { action: "ignored", reason: "duplicate", event, eventKey } });
12288
+ if (asyncRouting) {
12289
+ runGitHubWebhookRouteInBackground({ payload, event, delivery, config, state: remembered.state, worktree, clickupClient, githubClient, openCodeClient, sendSessionEvent, verifySessionEventDelivery });
12290
+ return finish({ ok: true, status: 200, reason: "accepted", result: { action: "accepted", event, eventKey } });
12291
+ }
12292
+ const result = await routeGitHubWebhookEvent({ payload, event, delivery, config, state: remembered.state, worktree, clickupClient, githubClient, openCodeClient, sendSessionEvent, verifySessionEventDelivery });
12293
+ return finish({ ok: result.ok, status: result.ok ? 200 : 422, result });
12294
+ } catch (error) {
12295
+ writeClickUpWebhookAuditLog({ method, url, headers, rawBody, config, state, handled, error, payload, at: now() });
12296
+ throw error;
12297
+ }
12298
+ }
11938
12299
  async function handleClickUpWebhookRequest({ method = "POST", url = null, headers = {}, rawBody = "", config, state, worktree, clickupClient, openCodeClient, saveState, now = () => /* @__PURE__ */ new Date(), sessionExists, createSession, sendSessionEvent, ensureTaskWorktree, verifySessionEventDelivery, asyncRouting = false } = {}) {
11939
12300
  let payload = null;
11940
12301
  let handled = null;
@@ -11997,13 +12358,17 @@ function clickUpListenerFingerprint({ config, state, worktree, clickupClient, op
11997
12358
  teamId: config?.teamId || "",
11998
12359
  publicUrl: config?.webhook?.publicUrl || "",
11999
12360
  path: clickUpWebhookExpectedPath(config),
12361
+ githubPublicUrl: config?.github?.webhook?.publicUrl || "",
12362
+ githubPath: gitHubWebhookExpectedPath(config),
12363
+ githubEnabled: gitHubWebhookEnabled(config),
12364
+ githubEvents: config?.github?.webhook?.events || [],
12000
12365
  location: config?.webhook?.location || {},
12001
12366
  webhookId: state?.webhookId || "",
12002
12367
  secret: state?.secret || "",
12003
12368
  events: config?.webhook?.events || []
12004
12369
  });
12005
12370
  }
12006
- function startClickUpWebhookListener({ config, state, worktree, clickupClient, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
12371
+ function startClickUpWebhookListener({ config, state, worktree, clickupClient, githubClient = null, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
12007
12372
  if (!isClickUpWebhookStateActive(state, config)) return { active: false, ready: false, reason: "inactive_state" };
12008
12373
  const key = clickUpListenerKey(config);
12009
12374
  const fingerprint = clickUpListenerFingerprint({ config, state, worktree, clickupClient, openCodeClient });
@@ -12022,7 +12387,9 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
12022
12387
  return;
12023
12388
  }
12024
12389
  const requestPath = new URL(req.url || "/", config.webhook.publicUrl).pathname;
12025
- if (requestPath !== clickUpWebhookExpectedPath(config)) {
12390
+ const isClickUpPath = requestPath === clickUpWebhookExpectedPath(config);
12391
+ const isGitHubPath = gitHubWebhookEnabled(config) && requestPath === gitHubWebhookExpectedPath(config);
12392
+ if (!isClickUpPath && !isGitHubPath) {
12026
12393
  res.writeHead(404, { "Content-Type": "application/json" });
12027
12394
  res.end(JSON.stringify({ ok: false, reason: "wrong_path" }));
12028
12395
  return;
@@ -12049,7 +12416,7 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
12049
12416
  if (overflow || res.writableEnded) return;
12050
12417
  const latestState = readClickUpWebhookState(worktree, config);
12051
12418
  try {
12052
- const handled = await handleClickUpWebhookRequest({
12419
+ const common = {
12053
12420
  method: req.method,
12054
12421
  url: req.url,
12055
12422
  headers: req.headers,
@@ -12061,7 +12428,8 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
12061
12428
  openCodeClient,
12062
12429
  saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, config),
12063
12430
  asyncRouting: true
12064
- });
12431
+ };
12432
+ const handled = isGitHubPath ? await handleGitHubWebhookRequest({ ...common, githubClient }) : await handleClickUpWebhookRequest(common);
12065
12433
  res.writeHead(handled.status, { "Content-Type": "application/json" });
12066
12434
  res.end(JSON.stringify({ ok: handled.ok, reason: handled.reason, result: handled.result?.action }));
12067
12435
  } catch (error) {
@@ -13179,6 +13547,7 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
13179
13547
  const operatingTeamMode = getOperatingTeamMode(repoCfg);
13180
13548
  const clickUpWebhookValidation = normalizeClickUpWebhookConfig(pickConfiguredClickUp(input, repoCfg, resolvedPluginOptions), worktree);
13181
13549
  const runtimeClickUpClient = input.clickupClient || (clickUpWebhookValidation.config?.test === true ? createTestClickUpApiClient(clickUpWebhookValidation.config) : createClickUpApiClient(clickUpWebhookValidation.config, input.fetch));
13550
+ const runtimeGitHubClient = input.githubClient || (clickUpWebhookValidation.config?.github?.webhook?.enabled === true ? createGitHubApiClient(clickUpWebhookValidation.config.github, input.fetch) : null);
13182
13551
  let clickUpWebhookRuntime = { active: false, valid: false, reason: "not_configured" };
13183
13552
  if (clickUpWebhookValidation.complete) {
13184
13553
  try {
@@ -13203,6 +13572,7 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
13203
13572
  state: listenerState,
13204
13573
  worktree,
13205
13574
  clickupClient: lifecycleClickUpClient,
13575
+ githubClient: runtimeGitHubClient,
13206
13576
  openCodeClient: input.client,
13207
13577
  listenerRegistry
13208
13578
  });
@@ -13878,7 +14248,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
13878
14248
  }
13879
14249
  };
13880
14250
  }
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 };
14251
+ 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, createGitHubApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, gitHubPullRequestContextFromPayload, gitHubWebhookExpectedPath, handleClickUpWebhookRequest, handleGitHubWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, routeGitHubWebhookEvent, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, verifyGitHubSignature, 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 };
13882
14252
 
13883
14253
  // src/sanitize_cli.js
13884
14254
  var { migrateLegacyOptimaLayout: migrateLegacyOptimaLayout2 } = OptimaPlugin.__internals;
@@ -25,9 +25,11 @@
25
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
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.
27
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.
28
+ - Validation is not complete until the model leaves the current GitHub PR link visible in ClickUp with source branch, target branch, and validation owner; Optima runtime must still limit its own ClickUp writes to metadata.
28
29
  - `workflow_product_manager` is registered only when explicit ClickUp webhook mode is configured and the local webhook subscription state is active/valid.
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.
30
- - The listener is a gated in-process HTTP listener for local runtime use; deployments still need a stable public URL/tunnel and local `.optima/.config/runtime/` state containing the ClickUp webhook id/secret. When a ClickUp client exposes webhook validation, Optima validates remote state; otherwise local-only validation is limited to ignored runtime state with matching URL/events and present id/secret.
30
+ - Webhook mode is opt-in: Optima validates signed `X-Signature` HMAC SHA-256 ClickUp requests, routes status/assignee events only for Product Manager-assigned non-terminal tasks, routes comments only when they mention `@Defend Tech Product Manager`, and stores new `ses_...` ids in ClickUp `agent_metadata`. Runtime/process failures stay in local logs and must not create ClickUp comments or tags.
31
+ - The same gated in-process listener can also accept signed GitHub `X-Hub-Signature-256` PR/review/comment events at `/optima/github/webhook` when `clickup.github.webhook` is enabled. GitHub events resolve the ClickUp task from the PR source branch, update only `agent_metadata.task.github`, and steer the existing `workflow_product_manager` session; they do not create replacement sessions or ClickUp runtime comments.
32
+ - Deployments still need a stable public URL/tunnel and local `.optima/.config/runtime/` state containing the ClickUp webhook id/secret. When a ClickUp client exposes webhook validation, Optima validates remote state; otherwise local-only validation is limited to ignored runtime state with matching URL/events and present id/secret.
31
33
 
32
34
  ## Shared Worktree And Git
33
35
 
@@ -18,9 +18,10 @@
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
+ - Validation requires a model-owned ClickUp status comment with the current GitHub PR link, source branch, target branch, and validation owner; Optima runtime itself writes only metadata/logs.
21
22
  - `workflow_product_manager` is registered only when opt-in ClickUp webhook mode is complete and active/valid.
22
- - Webhook mode validates `X-Signature` HMAC SHA-256, routes only PM-assigned non-terminal status/assignee events, routes comments only on `@Defend Tech Product Manager`, writes new `ses_...` ids to `agent_metadata`, and reports stale/missing sessions to ClickUp without replacement.
23
- - The listener is gated in-process local runtime; production needs a stable public URL/tunnel and ignored `.optima/.config/runtime/` webhook state. Remote webhook validation is used when the injected ClickUp client supports it; otherwise local-only validation is limited to matching id/secret/URL/events state.
23
+ - Webhook mode validates ClickUp `X-Signature` HMAC SHA-256, routes only PM-assigned non-terminal status/assignee events, routes comments only on `@Defend Tech Product Manager`, writes new `ses_...` ids to `agent_metadata`, and keeps runtime/process failures in local logs, never ClickUp comments/tags.
24
+ - The listener is gated in-process local runtime; production needs a stable public URL/tunnel and ignored `.optima/.config/runtime/` webhook state. It can also accept GitHub `X-Hub-Signature-256` PR/review/comment webhooks at `/optima/github/webhook`; these update only `agent_metadata.task.github` and steer the existing WPM session resolved from the PR source branch.
24
25
  - Communication: questions, blockers, reviews, dependencies, and escalation go through PMA. Tech Lead reviews architecture/code/PR/standards/repo skills; Validator/QA verifies tests/regression/coverage/evidence/docs.
25
26
  - Reopen same-scope discrepancies by reactivating the same task file, adding `Reopen History`, and reusing existing Task/Workflow Runner session IDs when possible.
26
27
  - Blockers move to `.optima/tasks/blocked/` with a clear blocker report and PO-facing resolution need.
@@ -68,6 +68,7 @@ For ClickUp-first delivery, Validation is a GitHub PR state, not a comment-only
68
68
 
69
69
  - Subtasks open/update a PR from the subtask branch into the parent task branch before entering Validation.
70
70
  - Parent tasks open/update a PR from the task branch into `dev` before entering Validation.
71
+ - The model must leave the current PR link visible in ClickUp when it moves a task/subtask to Validation, including source/target branch and validation owner. Optima runtime still writes only metadata/logs; the ClickUp comment is model-owned work status.
71
72
  - 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
73
  - 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
74
  - If merge, Vercel deployment, or regression fails, create Bug subtasks under the parent and return the parent to `in progress`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defend-tech/opencode-optima",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+ssh://git@github.com/defend-tech/opencode-optima.git"