@defend-tech/opencode-optima 0.1.71 → 0.1.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/workflow_product_manager.md +7 -3
- package/dist/index.js +481 -16
- package/dist/sanitize_cli.js +481 -16
- package/docs/core/agent_orchestration.md +3 -2
- package/docs/core/agent_orchestration.prompt.md +2 -2
- package/docs/guides/AGENTS.md +11 -0
- package/package.json +1 -1
- package/templates/optima.yaml.template +19 -0
package/dist/sanitize_cli.js
CHANGED
|
@@ -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;
|
|
@@ -8826,6 +8827,70 @@ function deriveClickUpPrTarget({ isRelease = false, parentTaskId, parentBranch,
|
|
|
8826
8827
|
}
|
|
8827
8828
|
return devBranch;
|
|
8828
8829
|
}
|
|
8830
|
+
function deriveClickUpRequiredPullRequest({ taskId, taskType = "Tarea", parentTaskId = "", subtaskId = "", branch = "", parentBranch = "", devBranch = "dev", mainBranch = "main", isRelease = false, githubRemote = "origin", prUrl = "", prNumber = "" } = {}) {
|
|
8831
|
+
const effectiveParent = parentTaskId || taskId;
|
|
8832
|
+
const isSubtask = isClickUpSubtaskRoute({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
|
|
8833
|
+
const sourceBranch = branch || deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
|
|
8834
|
+
const targetBranch = deriveClickUpPrTarget({
|
|
8835
|
+
isRelease,
|
|
8836
|
+
parentBranch: isSubtask ? parentBranch || deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent }) : parentBranch,
|
|
8837
|
+
parentTaskId: isSubtask ? effectiveParent : "",
|
|
8838
|
+
parentTaskType: taskType,
|
|
8839
|
+
taskType,
|
|
8840
|
+
devBranch,
|
|
8841
|
+
mainBranch
|
|
8842
|
+
});
|
|
8843
|
+
return {
|
|
8844
|
+
required: true,
|
|
8845
|
+
sourceBranch,
|
|
8846
|
+
targetBranch,
|
|
8847
|
+
githubRemote,
|
|
8848
|
+
prUrl: String(prUrl || "").trim(),
|
|
8849
|
+
prNumber: String(prNumber || "").trim(),
|
|
8850
|
+
scope: isSubtask ? "subtask_to_parent" : isRelease ? "release_to_main" : "task_to_dev",
|
|
8851
|
+
policy: "validation_requires_open_pull_request"
|
|
8852
|
+
};
|
|
8853
|
+
}
|
|
8854
|
+
function buildClickUpReviewAutomationPlan({ isSubtask = false, humanApprover = "CTO", preproductionProvider = "Vercel", preproductionEnvironment = "preproduction" } = {}) {
|
|
8855
|
+
return {
|
|
8856
|
+
github_webhooks: {
|
|
8857
|
+
listen_for: ["pull_request_review", "pull_request_review_comment", "issue_comment", "pull_request"],
|
|
8858
|
+
comment_policy: "reply_before_change_and_after_change",
|
|
8859
|
+
change_request_status: "in progress",
|
|
8860
|
+
after_fix_status: "validation",
|
|
8861
|
+
state_storage: "ClickUp agent_metadata.github"
|
|
8862
|
+
},
|
|
8863
|
+
review_comments: [
|
|
8864
|
+
"Answer GitHub comments directly in GitHub.",
|
|
8865
|
+
"If a comment implies a code/doc/test change, first reply with the intended action, move the ClickUp task/subtask to in progress, implement, push the same branch, move it back to validation, update the PR, then reply again with what changed.",
|
|
8866
|
+
"Also update ClickUp with human-readable model work status; runtime/process noise stays in local logs."
|
|
8867
|
+
],
|
|
8868
|
+
accepted_review: {
|
|
8869
|
+
accepted_by: humanApprover,
|
|
8870
|
+
trigger: "approved_pull_request_review",
|
|
8871
|
+
action: isSubtask ? "merge_subtask_pr_into_parent_branch" : "merge_parent_pr_into_dev",
|
|
8872
|
+
delete_source_branch_after_success: true,
|
|
8873
|
+
delete_worktree_after_success: true,
|
|
8874
|
+
preserve_opencode_sessions_in_agent_metadata: true
|
|
8875
|
+
},
|
|
8876
|
+
preproduction: isSubtask ? {
|
|
8877
|
+
required: false,
|
|
8878
|
+
reason: "Subtask merge stops at parent branch; parent task owns dev/preproduction."
|
|
8879
|
+
} : {
|
|
8880
|
+
required: true,
|
|
8881
|
+
provider: preproductionProvider,
|
|
8882
|
+
environment: preproductionEnvironment,
|
|
8883
|
+
verify_deployment_after_merge: true,
|
|
8884
|
+
run_smoke_regression: true,
|
|
8885
|
+
on_failure: "create_bug_subtasks_and_return_parent_to_in_progress",
|
|
8886
|
+
completion_gate: "preproduction_deployed_and_regression_passed"
|
|
8887
|
+
},
|
|
8888
|
+
completion: {
|
|
8889
|
+
final_clickup_comment_required: true,
|
|
8890
|
+
final_status: isSubtask ? "completed_or_parent_validation" : "completed"
|
|
8891
|
+
}
|
|
8892
|
+
};
|
|
8893
|
+
}
|
|
8829
8894
|
function validateMainWorkspaceBranchSafety({ currentBranch, requiredBranch = "dev", forbiddenBranch = "main" } = {}) {
|
|
8830
8895
|
const normalized = String(currentBranch ?? "").trim();
|
|
8831
8896
|
if (!normalized) {
|
|
@@ -9488,6 +9553,7 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9488
9553
|
const worktree = deriveClickUpWorktree({ baseWorktree, taskId, taskType, parentTaskId: effectiveParent, subtaskId });
|
|
9489
9554
|
const prTarget = isSubtask ? parentBranch : "dev";
|
|
9490
9555
|
const startFrom = isSubtask ? parentBranch : "dev";
|
|
9556
|
+
const requiredPullRequest = deriveClickUpRequiredPullRequest({ taskId, taskType, parentTaskId: effectiveParent, subtaskId, branch, parentBranch });
|
|
9491
9557
|
const estimate = preEstimateClickUpWork({ complexity, filesChanged, acceptanceCriteria, unknowns });
|
|
9492
9558
|
const metadata = mergeClickUpAgentMetadata(existingAgentMetadata, {
|
|
9493
9559
|
task: {
|
|
@@ -9501,6 +9567,10 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9501
9567
|
mirror_task_path: `.optima/tasks/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}.md`,
|
|
9502
9568
|
evidence_path: `.optima/evidences/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}/SUMMARY.md`,
|
|
9503
9569
|
delivery_evidence_path: deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent),
|
|
9570
|
+
github: {
|
|
9571
|
+
required_pull_request: requiredPullRequest,
|
|
9572
|
+
review_automation: buildClickUpReviewAutomationPlan({ isSubtask })
|
|
9573
|
+
},
|
|
9504
9574
|
...agentMetadata
|
|
9505
9575
|
}
|
|
9506
9576
|
});
|
|
@@ -9523,6 +9593,10 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9523
9593
|
parentBranch: parentBranch || void 0,
|
|
9524
9594
|
prTarget
|
|
9525
9595
|
},
|
|
9596
|
+
github: {
|
|
9597
|
+
requiredPullRequest,
|
|
9598
|
+
reviewAutomation: buildClickUpReviewAutomationPlan({ isSubtask })
|
|
9599
|
+
},
|
|
9526
9600
|
mirror: {
|
|
9527
9601
|
taskPath: `.optima/tasks/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}.md`,
|
|
9528
9602
|
evidenceSummaryPath: `.optima/evidences/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}/SUMMARY.md`,
|
|
@@ -9533,14 +9607,15 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9533
9607
|
wouldCreate: true
|
|
9534
9608
|
},
|
|
9535
9609
|
clickup: {
|
|
9536
|
-
comment: `Starting task from ${startFrom}. Branch: ${branch}. Worktree: ${worktree}. Delivery evidence: ${deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent)}.`,
|
|
9610
|
+
comment: `Starting task from ${startFrom}. Branch: ${branch}. Worktree: ${worktree}. Validation PR target: ${requiredPullRequest.targetBranch}. Delivery evidence: ${deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent)}.`,
|
|
9537
9611
|
description,
|
|
9538
9612
|
fields: {
|
|
9539
9613
|
Definition: definitionContent,
|
|
9540
9614
|
"Story Points": estimate.storyPoints,
|
|
9541
9615
|
agent_metadata: metadata,
|
|
9542
9616
|
branch,
|
|
9543
|
-
worktree
|
|
9617
|
+
worktree,
|
|
9618
|
+
pr_target: requiredPullRequest.targetBranch
|
|
9544
9619
|
},
|
|
9545
9620
|
definition_doc: {
|
|
9546
9621
|
parent: normalizeClickUpDefinitionDocParent(definitionDocParent),
|
|
@@ -9550,23 +9625,29 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9550
9625
|
}
|
|
9551
9626
|
};
|
|
9552
9627
|
}
|
|
9553
|
-
function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed = false, validationFailed = false, isSubtask = false, reopen = false, assignees = [], removeAssignees = [], productManagerAssignee = "", requireProductManagerAssignee = true, planDescription = "", definition = "", definitionDocParent = CLICKUP_DEFINITION_DOC_PARENT, humansRegistry } = {}) {
|
|
9628
|
+
function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed = false, validationFailed = false, isSubtask = false, reopen = false, assignees = [], removeAssignees = [], productManagerAssignee = "", requireProductManagerAssignee = true, planDescription = "", definition = "", definitionDocParent = CLICKUP_DEFINITION_DOC_PARENT, humansRegistry, taskId = "", taskType = "Tarea", parentTaskId = "", subtaskId = "", branch = "", parentBranch = "", prUrl = "", prNumber = "", githubRemote = "origin", devBranch = "dev", mainBranch = "main", preproductionProvider = "Vercel", preproductionEnvironment = "preproduction" } = {}) {
|
|
9554
9629
|
const from = normalizeClickUpStatus(fromStatus);
|
|
9630
|
+
const effectiveIsSubtask = Boolean(isSubtask || isClickUpSubtaskRoute({ taskType, parentTaskId: parentTaskId || taskId, subtaskId, taskId }));
|
|
9555
9631
|
let to = normalizeClickUpStatus(toStatus);
|
|
9556
9632
|
if (validationFailed) to = "in progress";
|
|
9557
|
-
if (!to && validationPassed) to =
|
|
9633
|
+
if (!to && validationPassed) to = effectiveIsSubtask ? "completed" : "merge";
|
|
9558
9634
|
if ((from === "completed" || from === "closed") && !reopen) {
|
|
9559
9635
|
return { ok: true, noop: true, dryRun: true, message: "Completed/closed task is a no-op unless reopen is explicit." };
|
|
9560
9636
|
}
|
|
9561
9637
|
const key = `${from}->${to}`;
|
|
9562
9638
|
const rule = CLICKUP_TRANSITIONS.get(key);
|
|
9563
9639
|
if (!rule) return { ok: false, dryRun: true, message: `Transition not allowed: ${key}` };
|
|
9564
|
-
const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval &&
|
|
9640
|
+
const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && effectiveIsSubtask);
|
|
9565
9641
|
const requiresPlanCompletionContract = from === "plan" && to === "in progress";
|
|
9642
|
+
const requiresValidationPullRequest = from === "in progress" && to === "validation";
|
|
9566
9643
|
const definitionContent = compactMarkdownValue(definition);
|
|
9567
9644
|
const description = compactMarkdownValue(planDescription);
|
|
9645
|
+
const canDerivePullRequest = Boolean(taskId || branch);
|
|
9646
|
+
const shouldAttachPullRequest = requiresValidationPullRequest || (from === "validation" || to === "merge" || from === "merge") && canDerivePullRequest;
|
|
9647
|
+
const requiredPullRequest = shouldAttachPullRequest && canDerivePullRequest ? deriveClickUpRequiredPullRequest({ taskId, taskType, parentTaskId, subtaskId, branch, parentBranch, devBranch, mainBranch, githubRemote, prUrl, prNumber }) : null;
|
|
9648
|
+
const reviewAutomation = requiredPullRequest ? buildClickUpReviewAutomationPlan({ isSubtask: effectiveIsSubtask, preproductionProvider, preproductionEnvironment }) : null;
|
|
9568
9649
|
const validationErrors = [];
|
|
9569
|
-
if (rule.parentOnlyFinalApproval &&
|
|
9650
|
+
if (rule.parentOnlyFinalApproval && effectiveIsSubtask) {
|
|
9570
9651
|
validationErrors.push("Subtasks must not use the parent final-approval transition; merge validated subtasks directly into the parent branch/workspace.");
|
|
9571
9652
|
}
|
|
9572
9653
|
if (assignsFinalApprovers && !isRealClickUpAssigneeId(productManagerAssignee) && requireProductManagerAssignee !== false) {
|
|
@@ -9576,19 +9657,34 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
|
|
|
9576
9657
|
if (!description) validationErrors.push("planDescription is required for the plan-completion ClickUp task description rewrite.");
|
|
9577
9658
|
if (!definitionContent) validationErrors.push("definition is required and must contain the complete plan/Definition content at plan completion.");
|
|
9578
9659
|
}
|
|
9660
|
+
if (requiresValidationPullRequest) {
|
|
9661
|
+
if (!requiredPullRequest) validationErrors.push("taskId or branch is required before moving a ClickUp task to validation.");
|
|
9662
|
+
if (!requiredPullRequest?.sourceBranch) validationErrors.push("branch/sourceBranch is required before moving a ClickUp task to validation.");
|
|
9663
|
+
if (!requiredPullRequest?.targetBranch) validationErrors.push("PR target branch is required before moving a ClickUp task to validation.");
|
|
9664
|
+
if (!requiredPullRequest?.prUrl && !requiredPullRequest?.prNumber) validationErrors.push("An open GitHub pull request URL or number is required before moving a ClickUp task to validation.");
|
|
9665
|
+
}
|
|
9579
9666
|
if (validationErrors.length > 0) return clickUpPayloadValidationError(validationErrors);
|
|
9580
9667
|
const finalApprovers = assignsFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
|
|
9581
9668
|
const explicitRemovals = (removeAssignees || []).filter(Boolean);
|
|
9582
9669
|
const normalizedProductManagerAssignee = isRealClickUpAssigneeId(productManagerAssignee) ? String(productManagerAssignee).trim() : "";
|
|
9583
9670
|
const removalTargets = assignsFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
|
|
9584
9671
|
const assigned = [...new Set([...assignees || [], ...finalApprovers].filter(Boolean))].filter((assignee) => !removalTargets.includes(assignee));
|
|
9585
|
-
const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
|
|
9672
|
+
const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask: effectiveIsSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
|
|
9586
9673
|
const fields = authority ? { merge_authority: JSON.stringify(authority) } : {};
|
|
9674
|
+
if (requiredPullRequest) {
|
|
9675
|
+
fields.github_pull_request = JSON.stringify(requiredPullRequest);
|
|
9676
|
+
fields.github_review_automation = JSON.stringify(reviewAutomation);
|
|
9677
|
+
}
|
|
9587
9678
|
if (definitionContent) fields.Definition = definitionContent;
|
|
9588
9679
|
return {
|
|
9589
9680
|
ok: true,
|
|
9590
9681
|
dryRun: true,
|
|
9591
|
-
transition: { fromStatus: from, toStatus: rule.status, validationPassed, validationFailed, isSubtask },
|
|
9682
|
+
transition: { fromStatus: from, toStatus: rule.status, validationPassed, validationFailed, isSubtask: effectiveIsSubtask },
|
|
9683
|
+
github: requiredPullRequest ? {
|
|
9684
|
+
requiredPullRequest,
|
|
9685
|
+
reviewAutomation
|
|
9686
|
+
} : void 0,
|
|
9687
|
+
vercel: reviewAutomation?.preproduction?.required ? reviewAutomation.preproduction : void 0,
|
|
9592
9688
|
clickup: {
|
|
9593
9689
|
status: rule.status,
|
|
9594
9690
|
assignees: assigned,
|
|
@@ -9792,9 +9888,48 @@ function normalizeNonNegativeInteger(value, defaultValue) {
|
|
|
9792
9888
|
const number = Number(value);
|
|
9793
9889
|
return Number.isInteger(number) && number >= 0 ? number : defaultValue;
|
|
9794
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
|
+
}
|
|
9795
9929
|
function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd()) {
|
|
9796
9930
|
const raw = isPlainObject(rawClickUp) ? rawClickUp : {};
|
|
9797
9931
|
const webhook = isPlainObject(raw.webhook) ? raw.webhook : {};
|
|
9932
|
+
const github = normalizeGitHubWebhookConfig(raw.github, { publicUrl: String(webhook.public_url || webhook.publicUrl || "").trim() });
|
|
9798
9933
|
const routing = isPlainObject(raw.routing) ? raw.routing : {};
|
|
9799
9934
|
const opencode = isPlainObject(raw.opencode) ? raw.opencode : {};
|
|
9800
9935
|
const openchamber = isPlainObject(raw.openchamber) ? raw.openchamber : {};
|
|
@@ -9847,6 +9982,7 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
|
|
|
9847
9982
|
},
|
|
9848
9983
|
events
|
|
9849
9984
|
},
|
|
9985
|
+
github,
|
|
9850
9986
|
routing: {
|
|
9851
9987
|
targetAgent: String(routing.target_agent || routing.targetAgent || "workflow_product_manager").trim(),
|
|
9852
9988
|
productManagerAssigneeId: String(routing.product_manager_assignee_id || routing.productManagerAssigneeId || "").trim(),
|
|
@@ -9868,6 +10004,13 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
|
|
|
9868
10004
|
if (!config.webhook.bindHost) errors.push("clickup.webhook.bind_host is required");
|
|
9869
10005
|
if (!Number.isInteger(config.webhook.bindPort) || config.webhook.bindPort <= 0) errors.push("clickup.webhook.bind_port must be a positive integer");
|
|
9870
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
|
+
}
|
|
9871
10014
|
if (config.opencode.baseUrl) {
|
|
9872
10015
|
try {
|
|
9873
10016
|
new URL(config.opencode.baseUrl);
|
|
@@ -9905,6 +10048,8 @@ function sanitizeClickUpWebhookState(state = {}, config = null) {
|
|
|
9905
10048
|
listener: isPlainObject(state.listener) ? state.listener : {},
|
|
9906
10049
|
pendingSessions: isPlainObject(state.pendingSessions) ? state.pendingSessions : {},
|
|
9907
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) : [],
|
|
9908
10053
|
recentEventKeys
|
|
9909
10054
|
};
|
|
9910
10055
|
}
|
|
@@ -10021,6 +10166,29 @@ function createClickUpApiClient(config, fetchImpl = globalThis.fetch) {
|
|
|
10021
10166
|
}
|
|
10022
10167
|
};
|
|
10023
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
|
+
}
|
|
10024
10192
|
function createTestClickUpApiClient(config) {
|
|
10025
10193
|
const metadata = /* @__PURE__ */ new Map();
|
|
10026
10194
|
return {
|
|
@@ -10251,6 +10419,19 @@ function verifyClickUpSignature(rawBody, signatureHeader, secret) {
|
|
|
10251
10419
|
const wanted = Buffer.from(expected, "hex");
|
|
10252
10420
|
return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
|
|
10253
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
|
+
}
|
|
10254
10435
|
async function resyncClickUpWebhookForSignatureDrift({ rawBody, signature, config, state = {}, worktree = process.cwd(), clickupClient, saveState } = {}) {
|
|
10255
10436
|
const validation = clickUpWebhookValidationFromConfig(config);
|
|
10256
10437
|
if (!clickupClient?.listWebhooks && !clickupClient?.createWebhook) {
|
|
@@ -10457,6 +10638,105 @@ function clickUpTaskAgentMetadata(task = {}, fieldId = "") {
|
|
|
10457
10638
|
const field = fields.find((item) => String(item?.id || "") === fieldId || String(item?.name || "") === "agent_metadata");
|
|
10458
10639
|
return field?.value || "";
|
|
10459
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
|
+
}
|
|
10460
10740
|
function getNestedMetadataValue(metadata, dottedKey) {
|
|
10461
10741
|
const parts = String(dottedKey || "").split(".").filter(Boolean);
|
|
10462
10742
|
let cursor = metadata;
|
|
@@ -11362,9 +11642,9 @@ function buildClickUpWebhookAuditSummary({ method, url, config, handled, error,
|
|
|
11362
11642
|
ok: Boolean(handled?.ok),
|
|
11363
11643
|
reason: handled?.reason || result.reason || (error ? "handler_error" : void 0),
|
|
11364
11644
|
action: result.action,
|
|
11365
|
-
event: payload ? clickUpEventType(payload) : void 0,
|
|
11645
|
+
event: result.event || (payload ? clickUpEventType(payload) : void 0),
|
|
11366
11646
|
task: result.taskId || clickUpTaskIdFromPayload(payload || {}),
|
|
11367
|
-
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,
|
|
11368
11648
|
session: result.sessionId,
|
|
11369
11649
|
base_path: config?.basePath || "",
|
|
11370
11650
|
request_file: requestFile || void 0
|
|
@@ -11378,7 +11658,13 @@ function writeClickUpWebhookAuditLog({ method, url, headers = {}, rawBody = "",
|
|
|
11378
11658
|
if (level === "error" && !failed) return;
|
|
11379
11659
|
const logDir = clickUpWebhookAuditLogDir();
|
|
11380
11660
|
fs5.mkdirSync(logDir, { recursive: true });
|
|
11381
|
-
const secretValues = [
|
|
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);
|
|
11382
11668
|
let requestFile;
|
|
11383
11669
|
if (level === "verbose") {
|
|
11384
11670
|
const absoluteRequestFile = clickUpWebhookRequestFilePath(at, logDir);
|
|
@@ -11840,6 +12126,176 @@ function runClickUpWebhookRouteInBackground({ payload, config, state, worktree,
|
|
|
11840
12126
|
});
|
|
11841
12127
|
return route;
|
|
11842
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
|
+
}
|
|
11843
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 } = {}) {
|
|
11844
12300
|
let payload = null;
|
|
11845
12301
|
let handled = null;
|
|
@@ -11902,13 +12358,17 @@ function clickUpListenerFingerprint({ config, state, worktree, clickupClient, op
|
|
|
11902
12358
|
teamId: config?.teamId || "",
|
|
11903
12359
|
publicUrl: config?.webhook?.publicUrl || "",
|
|
11904
12360
|
path: clickUpWebhookExpectedPath(config),
|
|
12361
|
+
githubPublicUrl: config?.github?.webhook?.publicUrl || "",
|
|
12362
|
+
githubPath: gitHubWebhookExpectedPath(config),
|
|
12363
|
+
githubEnabled: gitHubWebhookEnabled(config),
|
|
12364
|
+
githubEvents: config?.github?.webhook?.events || [],
|
|
11905
12365
|
location: config?.webhook?.location || {},
|
|
11906
12366
|
webhookId: state?.webhookId || "",
|
|
11907
12367
|
secret: state?.secret || "",
|
|
11908
12368
|
events: config?.webhook?.events || []
|
|
11909
12369
|
});
|
|
11910
12370
|
}
|
|
11911
|
-
function startClickUpWebhookListener({ config, state, worktree, clickupClient, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
|
|
12371
|
+
function startClickUpWebhookListener({ config, state, worktree, clickupClient, githubClient = null, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
|
|
11912
12372
|
if (!isClickUpWebhookStateActive(state, config)) return { active: false, ready: false, reason: "inactive_state" };
|
|
11913
12373
|
const key = clickUpListenerKey(config);
|
|
11914
12374
|
const fingerprint = clickUpListenerFingerprint({ config, state, worktree, clickupClient, openCodeClient });
|
|
@@ -11927,7 +12387,9 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
|
|
|
11927
12387
|
return;
|
|
11928
12388
|
}
|
|
11929
12389
|
const requestPath = new URL(req.url || "/", config.webhook.publicUrl).pathname;
|
|
11930
|
-
|
|
12390
|
+
const isClickUpPath = requestPath === clickUpWebhookExpectedPath(config);
|
|
12391
|
+
const isGitHubPath = gitHubWebhookEnabled(config) && requestPath === gitHubWebhookExpectedPath(config);
|
|
12392
|
+
if (!isClickUpPath && !isGitHubPath) {
|
|
11931
12393
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
11932
12394
|
res.end(JSON.stringify({ ok: false, reason: "wrong_path" }));
|
|
11933
12395
|
return;
|
|
@@ -11954,7 +12416,7 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
|
|
|
11954
12416
|
if (overflow || res.writableEnded) return;
|
|
11955
12417
|
const latestState = readClickUpWebhookState(worktree, config);
|
|
11956
12418
|
try {
|
|
11957
|
-
const
|
|
12419
|
+
const common = {
|
|
11958
12420
|
method: req.method,
|
|
11959
12421
|
url: req.url,
|
|
11960
12422
|
headers: req.headers,
|
|
@@ -11966,7 +12428,8 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
|
|
|
11966
12428
|
openCodeClient,
|
|
11967
12429
|
saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, config),
|
|
11968
12430
|
asyncRouting: true
|
|
11969
|
-
}
|
|
12431
|
+
};
|
|
12432
|
+
const handled = isGitHubPath ? await handleGitHubWebhookRequest({ ...common, githubClient }) : await handleClickUpWebhookRequest(common);
|
|
11970
12433
|
res.writeHead(handled.status, { "Content-Type": "application/json" });
|
|
11971
12434
|
res.end(JSON.stringify({ ok: handled.ok, reason: handled.reason, result: handled.result?.action }));
|
|
11972
12435
|
} catch (error) {
|
|
@@ -13084,6 +13547,7 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
13084
13547
|
const operatingTeamMode = getOperatingTeamMode(repoCfg);
|
|
13085
13548
|
const clickUpWebhookValidation = normalizeClickUpWebhookConfig(pickConfiguredClickUp(input, repoCfg, resolvedPluginOptions), worktree);
|
|
13086
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);
|
|
13087
13551
|
let clickUpWebhookRuntime = { active: false, valid: false, reason: "not_configured" };
|
|
13088
13552
|
if (clickUpWebhookValidation.complete) {
|
|
13089
13553
|
try {
|
|
@@ -13108,6 +13572,7 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
13108
13572
|
state: listenerState,
|
|
13109
13573
|
worktree,
|
|
13110
13574
|
clickupClient: lifecycleClickUpClient,
|
|
13575
|
+
githubClient: runtimeGitHubClient,
|
|
13111
13576
|
openCodeClient: input.client,
|
|
13112
13577
|
listenerRegistry
|
|
13113
13578
|
});
|
|
@@ -13783,7 +14248,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
|
|
|
13783
14248
|
}
|
|
13784
14249
|
};
|
|
13785
14250
|
}
|
|
13786
|
-
OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, compactPromptPath, createClickUpApiClient, createTestClickUpApiClient, createOpenCodeSession, createOpenCodeSessionControl, deliveryEvidencePathForClickUpTask, ensureClickUpTaskWorktree, ensureClickUpTaskWorktreeForWebhook, ensureClickUpTaskWorktreeOpenChamber, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, inspectOpenCodeSessionActivity, isClickUpDerivedWorktreeSibling, isClickUpSubtaskRoute, isClickUpWebhookStateActive, isSameOrNestedPath, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, openCodeSessionExists, promptOpenCodeSessionControl, probeOpenCodeSessionControl, readClickUpCommentLedger, readClickUpWebhookState, readOpenCodeSessionControl, readOpenCodeSessionMessages, reconcileClickUpStartup, registerOpenChamberClickUpWorktree, scheduleClickUpAssignmentWatchdog, scheduleClickUpStartupReconciliation, syncOpenChamberWorktreeVisibility, waitForOpenCodeReadiness, recordClickUpCommentVersionProcessed, resyncClickUpWebhookForSignatureDrift, resolveIncludeFile, resolveIncludes, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, summarizeOpenCodeMessages, verifyOpenCodeSessionEventDelivery, stableClickUpCommentVersionMarker, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatRepairResult, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, optimaRepairDependencies, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, planOptimaRepair, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
|
|
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 };
|
|
13787
14252
|
|
|
13788
14253
|
// src/sanitize_cli.js
|
|
13789
14254
|
var { migrateLegacyOptimaLayout: migrateLegacyOptimaLayout2 } = OptimaPlugin.__internals;
|