@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/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;
|
|
@@ -8819,6 +8820,70 @@ function deriveClickUpPrTarget({ isRelease = false, parentTaskId, parentBranch,
|
|
|
8819
8820
|
}
|
|
8820
8821
|
return devBranch;
|
|
8821
8822
|
}
|
|
8823
|
+
function deriveClickUpRequiredPullRequest({ taskId, taskType = "Tarea", parentTaskId = "", subtaskId = "", branch = "", parentBranch = "", devBranch = "dev", mainBranch = "main", isRelease = false, githubRemote = "origin", prUrl = "", prNumber = "" } = {}) {
|
|
8824
|
+
const effectiveParent = parentTaskId || taskId;
|
|
8825
|
+
const isSubtask = isClickUpSubtaskRoute({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
|
|
8826
|
+
const sourceBranch = branch || deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
|
|
8827
|
+
const targetBranch = deriveClickUpPrTarget({
|
|
8828
|
+
isRelease,
|
|
8829
|
+
parentBranch: isSubtask ? parentBranch || deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent }) : parentBranch,
|
|
8830
|
+
parentTaskId: isSubtask ? effectiveParent : "",
|
|
8831
|
+
parentTaskType: taskType,
|
|
8832
|
+
taskType,
|
|
8833
|
+
devBranch,
|
|
8834
|
+
mainBranch
|
|
8835
|
+
});
|
|
8836
|
+
return {
|
|
8837
|
+
required: true,
|
|
8838
|
+
sourceBranch,
|
|
8839
|
+
targetBranch,
|
|
8840
|
+
githubRemote,
|
|
8841
|
+
prUrl: String(prUrl || "").trim(),
|
|
8842
|
+
prNumber: String(prNumber || "").trim(),
|
|
8843
|
+
scope: isSubtask ? "subtask_to_parent" : isRelease ? "release_to_main" : "task_to_dev",
|
|
8844
|
+
policy: "validation_requires_open_pull_request"
|
|
8845
|
+
};
|
|
8846
|
+
}
|
|
8847
|
+
function buildClickUpReviewAutomationPlan({ isSubtask = false, humanApprover = "CTO", preproductionProvider = "Vercel", preproductionEnvironment = "preproduction" } = {}) {
|
|
8848
|
+
return {
|
|
8849
|
+
github_webhooks: {
|
|
8850
|
+
listen_for: ["pull_request_review", "pull_request_review_comment", "issue_comment", "pull_request"],
|
|
8851
|
+
comment_policy: "reply_before_change_and_after_change",
|
|
8852
|
+
change_request_status: "in progress",
|
|
8853
|
+
after_fix_status: "validation",
|
|
8854
|
+
state_storage: "ClickUp agent_metadata.github"
|
|
8855
|
+
},
|
|
8856
|
+
review_comments: [
|
|
8857
|
+
"Answer GitHub comments directly in GitHub.",
|
|
8858
|
+
"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.",
|
|
8859
|
+
"Also update ClickUp with human-readable model work status; runtime/process noise stays in local logs."
|
|
8860
|
+
],
|
|
8861
|
+
accepted_review: {
|
|
8862
|
+
accepted_by: humanApprover,
|
|
8863
|
+
trigger: "approved_pull_request_review",
|
|
8864
|
+
action: isSubtask ? "merge_subtask_pr_into_parent_branch" : "merge_parent_pr_into_dev",
|
|
8865
|
+
delete_source_branch_after_success: true,
|
|
8866
|
+
delete_worktree_after_success: true,
|
|
8867
|
+
preserve_opencode_sessions_in_agent_metadata: true
|
|
8868
|
+
},
|
|
8869
|
+
preproduction: isSubtask ? {
|
|
8870
|
+
required: false,
|
|
8871
|
+
reason: "Subtask merge stops at parent branch; parent task owns dev/preproduction."
|
|
8872
|
+
} : {
|
|
8873
|
+
required: true,
|
|
8874
|
+
provider: preproductionProvider,
|
|
8875
|
+
environment: preproductionEnvironment,
|
|
8876
|
+
verify_deployment_after_merge: true,
|
|
8877
|
+
run_smoke_regression: true,
|
|
8878
|
+
on_failure: "create_bug_subtasks_and_return_parent_to_in_progress",
|
|
8879
|
+
completion_gate: "preproduction_deployed_and_regression_passed"
|
|
8880
|
+
},
|
|
8881
|
+
completion: {
|
|
8882
|
+
final_clickup_comment_required: true,
|
|
8883
|
+
final_status: isSubtask ? "completed_or_parent_validation" : "completed"
|
|
8884
|
+
}
|
|
8885
|
+
};
|
|
8886
|
+
}
|
|
8822
8887
|
function validateMainWorkspaceBranchSafety({ currentBranch, requiredBranch = "dev", forbiddenBranch = "main" } = {}) {
|
|
8823
8888
|
const normalized = String(currentBranch ?? "").trim();
|
|
8824
8889
|
if (!normalized) {
|
|
@@ -9481,6 +9546,7 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9481
9546
|
const worktree = deriveClickUpWorktree({ baseWorktree, taskId, taskType, parentTaskId: effectiveParent, subtaskId });
|
|
9482
9547
|
const prTarget = isSubtask ? parentBranch : "dev";
|
|
9483
9548
|
const startFrom = isSubtask ? parentBranch : "dev";
|
|
9549
|
+
const requiredPullRequest = deriveClickUpRequiredPullRequest({ taskId, taskType, parentTaskId: effectiveParent, subtaskId, branch, parentBranch });
|
|
9484
9550
|
const estimate = preEstimateClickUpWork({ complexity, filesChanged, acceptanceCriteria, unknowns });
|
|
9485
9551
|
const metadata = mergeClickUpAgentMetadata(existingAgentMetadata, {
|
|
9486
9552
|
task: {
|
|
@@ -9494,6 +9560,10 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9494
9560
|
mirror_task_path: `.optima/tasks/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}.md`,
|
|
9495
9561
|
evidence_path: `.optima/evidences/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}/SUMMARY.md`,
|
|
9496
9562
|
delivery_evidence_path: deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent),
|
|
9563
|
+
github: {
|
|
9564
|
+
required_pull_request: requiredPullRequest,
|
|
9565
|
+
review_automation: buildClickUpReviewAutomationPlan({ isSubtask })
|
|
9566
|
+
},
|
|
9497
9567
|
...agentMetadata
|
|
9498
9568
|
}
|
|
9499
9569
|
});
|
|
@@ -9516,6 +9586,10 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9516
9586
|
parentBranch: parentBranch || void 0,
|
|
9517
9587
|
prTarget
|
|
9518
9588
|
},
|
|
9589
|
+
github: {
|
|
9590
|
+
requiredPullRequest,
|
|
9591
|
+
reviewAutomation: buildClickUpReviewAutomationPlan({ isSubtask })
|
|
9592
|
+
},
|
|
9519
9593
|
mirror: {
|
|
9520
9594
|
taskPath: `.optima/tasks/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}.md`,
|
|
9521
9595
|
evidenceSummaryPath: `.optima/evidences/${branchSafeClickUpId(taskId || subtaskId || effectiveParent)}/SUMMARY.md`,
|
|
@@ -9526,14 +9600,15 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9526
9600
|
wouldCreate: true
|
|
9527
9601
|
},
|
|
9528
9602
|
clickup: {
|
|
9529
|
-
comment: `Starting task from ${startFrom}. Branch: ${branch}. Worktree: ${worktree}. Delivery evidence: ${deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent)}.`,
|
|
9603
|
+
comment: `Starting task from ${startFrom}. Branch: ${branch}. Worktree: ${worktree}. Validation PR target: ${requiredPullRequest.targetBranch}. Delivery evidence: ${deliveryEvidencePathForClickUpTask(taskId || subtaskId || effectiveParent)}.`,
|
|
9530
9604
|
description,
|
|
9531
9605
|
fields: {
|
|
9532
9606
|
Definition: definitionContent,
|
|
9533
9607
|
"Story Points": estimate.storyPoints,
|
|
9534
9608
|
agent_metadata: metadata,
|
|
9535
9609
|
branch,
|
|
9536
|
-
worktree
|
|
9610
|
+
worktree,
|
|
9611
|
+
pr_target: requiredPullRequest.targetBranch
|
|
9537
9612
|
},
|
|
9538
9613
|
definition_doc: {
|
|
9539
9614
|
parent: normalizeClickUpDefinitionDocParent(definitionDocParent),
|
|
@@ -9543,23 +9618,29 @@ function buildClickUpStartTaskPayload({ taskId, taskType = "Tarea", parentTaskId
|
|
|
9543
9618
|
}
|
|
9544
9619
|
};
|
|
9545
9620
|
}
|
|
9546
|
-
function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed = false, validationFailed = false, isSubtask = false, reopen = false, assignees = [], removeAssignees = [], productManagerAssignee = "", requireProductManagerAssignee = true, planDescription = "", definition = "", definitionDocParent = CLICKUP_DEFINITION_DOC_PARENT, humansRegistry } = {}) {
|
|
9621
|
+
function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed = false, validationFailed = false, isSubtask = false, reopen = false, assignees = [], removeAssignees = [], productManagerAssignee = "", requireProductManagerAssignee = true, planDescription = "", definition = "", definitionDocParent = CLICKUP_DEFINITION_DOC_PARENT, humansRegistry, taskId = "", taskType = "Tarea", parentTaskId = "", subtaskId = "", branch = "", parentBranch = "", prUrl = "", prNumber = "", githubRemote = "origin", devBranch = "dev", mainBranch = "main", preproductionProvider = "Vercel", preproductionEnvironment = "preproduction" } = {}) {
|
|
9547
9622
|
const from = normalizeClickUpStatus(fromStatus);
|
|
9623
|
+
const effectiveIsSubtask = Boolean(isSubtask || isClickUpSubtaskRoute({ taskType, parentTaskId: parentTaskId || taskId, subtaskId, taskId }));
|
|
9548
9624
|
let to = normalizeClickUpStatus(toStatus);
|
|
9549
9625
|
if (validationFailed) to = "in progress";
|
|
9550
|
-
if (!to && validationPassed) to =
|
|
9626
|
+
if (!to && validationPassed) to = effectiveIsSubtask ? "completed" : "merge";
|
|
9551
9627
|
if ((from === "completed" || from === "closed") && !reopen) {
|
|
9552
9628
|
return { ok: true, noop: true, dryRun: true, message: "Completed/closed task is a no-op unless reopen is explicit." };
|
|
9553
9629
|
}
|
|
9554
9630
|
const key = `${from}->${to}`;
|
|
9555
9631
|
const rule = CLICKUP_TRANSITIONS.get(key);
|
|
9556
9632
|
if (!rule) return { ok: false, dryRun: true, message: `Transition not allowed: ${key}` };
|
|
9557
|
-
const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval &&
|
|
9633
|
+
const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && effectiveIsSubtask);
|
|
9558
9634
|
const requiresPlanCompletionContract = from === "plan" && to === "in progress";
|
|
9635
|
+
const requiresValidationPullRequest = from === "in progress" && to === "validation";
|
|
9559
9636
|
const definitionContent = compactMarkdownValue(definition);
|
|
9560
9637
|
const description = compactMarkdownValue(planDescription);
|
|
9638
|
+
const canDerivePullRequest = Boolean(taskId || branch);
|
|
9639
|
+
const shouldAttachPullRequest = requiresValidationPullRequest || (from === "validation" || to === "merge" || from === "merge") && canDerivePullRequest;
|
|
9640
|
+
const requiredPullRequest = shouldAttachPullRequest && canDerivePullRequest ? deriveClickUpRequiredPullRequest({ taskId, taskType, parentTaskId, subtaskId, branch, parentBranch, devBranch, mainBranch, githubRemote, prUrl, prNumber }) : null;
|
|
9641
|
+
const reviewAutomation = requiredPullRequest ? buildClickUpReviewAutomationPlan({ isSubtask: effectiveIsSubtask, preproductionProvider, preproductionEnvironment }) : null;
|
|
9561
9642
|
const validationErrors = [];
|
|
9562
|
-
if (rule.parentOnlyFinalApproval &&
|
|
9643
|
+
if (rule.parentOnlyFinalApproval && effectiveIsSubtask) {
|
|
9563
9644
|
validationErrors.push("Subtasks must not use the parent final-approval transition; merge validated subtasks directly into the parent branch/workspace.");
|
|
9564
9645
|
}
|
|
9565
9646
|
if (assignsFinalApprovers && !isRealClickUpAssigneeId(productManagerAssignee) && requireProductManagerAssignee !== false) {
|
|
@@ -9569,19 +9650,34 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
|
|
|
9569
9650
|
if (!description) validationErrors.push("planDescription is required for the plan-completion ClickUp task description rewrite.");
|
|
9570
9651
|
if (!definitionContent) validationErrors.push("definition is required and must contain the complete plan/Definition content at plan completion.");
|
|
9571
9652
|
}
|
|
9653
|
+
if (requiresValidationPullRequest) {
|
|
9654
|
+
if (!requiredPullRequest) validationErrors.push("taskId or branch is required before moving a ClickUp task to validation.");
|
|
9655
|
+
if (!requiredPullRequest?.sourceBranch) validationErrors.push("branch/sourceBranch is required before moving a ClickUp task to validation.");
|
|
9656
|
+
if (!requiredPullRequest?.targetBranch) validationErrors.push("PR target branch is required before moving a ClickUp task to validation.");
|
|
9657
|
+
if (!requiredPullRequest?.prUrl && !requiredPullRequest?.prNumber) validationErrors.push("An open GitHub pull request URL or number is required before moving a ClickUp task to validation.");
|
|
9658
|
+
}
|
|
9572
9659
|
if (validationErrors.length > 0) return clickUpPayloadValidationError(validationErrors);
|
|
9573
9660
|
const finalApprovers = assignsFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
|
|
9574
9661
|
const explicitRemovals = (removeAssignees || []).filter(Boolean);
|
|
9575
9662
|
const normalizedProductManagerAssignee = isRealClickUpAssigneeId(productManagerAssignee) ? String(productManagerAssignee).trim() : "";
|
|
9576
9663
|
const removalTargets = assignsFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
|
|
9577
9664
|
const assigned = [...new Set([...assignees || [], ...finalApprovers].filter(Boolean))].filter((assignee) => !removalTargets.includes(assignee));
|
|
9578
|
-
const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
|
|
9665
|
+
const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask: effectiveIsSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
|
|
9579
9666
|
const fields = authority ? { merge_authority: JSON.stringify(authority) } : {};
|
|
9667
|
+
if (requiredPullRequest) {
|
|
9668
|
+
fields.github_pull_request = JSON.stringify(requiredPullRequest);
|
|
9669
|
+
fields.github_review_automation = JSON.stringify(reviewAutomation);
|
|
9670
|
+
}
|
|
9580
9671
|
if (definitionContent) fields.Definition = definitionContent;
|
|
9581
9672
|
return {
|
|
9582
9673
|
ok: true,
|
|
9583
9674
|
dryRun: true,
|
|
9584
|
-
transition: { fromStatus: from, toStatus: rule.status, validationPassed, validationFailed, isSubtask },
|
|
9675
|
+
transition: { fromStatus: from, toStatus: rule.status, validationPassed, validationFailed, isSubtask: effectiveIsSubtask },
|
|
9676
|
+
github: requiredPullRequest ? {
|
|
9677
|
+
requiredPullRequest,
|
|
9678
|
+
reviewAutomation
|
|
9679
|
+
} : void 0,
|
|
9680
|
+
vercel: reviewAutomation?.preproduction?.required ? reviewAutomation.preproduction : void 0,
|
|
9585
9681
|
clickup: {
|
|
9586
9682
|
status: rule.status,
|
|
9587
9683
|
assignees: assigned,
|
|
@@ -9785,9 +9881,48 @@ function normalizeNonNegativeInteger(value, defaultValue) {
|
|
|
9785
9881
|
const number = Number(value);
|
|
9786
9882
|
return Number.isInteger(number) && number >= 0 ? number : defaultValue;
|
|
9787
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
|
+
}
|
|
9788
9922
|
function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd()) {
|
|
9789
9923
|
const raw = isPlainObject(rawClickUp) ? rawClickUp : {};
|
|
9790
9924
|
const webhook = isPlainObject(raw.webhook) ? raw.webhook : {};
|
|
9925
|
+
const github = normalizeGitHubWebhookConfig(raw.github, { publicUrl: String(webhook.public_url || webhook.publicUrl || "").trim() });
|
|
9791
9926
|
const routing = isPlainObject(raw.routing) ? raw.routing : {};
|
|
9792
9927
|
const opencode = isPlainObject(raw.opencode) ? raw.opencode : {};
|
|
9793
9928
|
const openchamber = isPlainObject(raw.openchamber) ? raw.openchamber : {};
|
|
@@ -9840,6 +9975,7 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
|
|
|
9840
9975
|
},
|
|
9841
9976
|
events
|
|
9842
9977
|
},
|
|
9978
|
+
github,
|
|
9843
9979
|
routing: {
|
|
9844
9980
|
targetAgent: String(routing.target_agent || routing.targetAgent || "workflow_product_manager").trim(),
|
|
9845
9981
|
productManagerAssigneeId: String(routing.product_manager_assignee_id || routing.productManagerAssigneeId || "").trim(),
|
|
@@ -9861,6 +9997,13 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
|
|
|
9861
9997
|
if (!config.webhook.bindHost) errors.push("clickup.webhook.bind_host is required");
|
|
9862
9998
|
if (!Number.isInteger(config.webhook.bindPort) || config.webhook.bindPort <= 0) errors.push("clickup.webhook.bind_port must be a positive integer");
|
|
9863
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
|
+
}
|
|
9864
10007
|
if (config.opencode.baseUrl) {
|
|
9865
10008
|
try {
|
|
9866
10009
|
new URL(config.opencode.baseUrl);
|
|
@@ -9898,6 +10041,8 @@ function sanitizeClickUpWebhookState(state = {}, config = null) {
|
|
|
9898
10041
|
listener: isPlainObject(state.listener) ? state.listener : {},
|
|
9899
10042
|
pendingSessions: isPlainObject(state.pendingSessions) ? state.pendingSessions : {},
|
|
9900
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) : [],
|
|
9901
10046
|
recentEventKeys
|
|
9902
10047
|
};
|
|
9903
10048
|
}
|
|
@@ -10014,6 +10159,29 @@ function createClickUpApiClient(config, fetchImpl = globalThis.fetch) {
|
|
|
10014
10159
|
}
|
|
10015
10160
|
};
|
|
10016
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
|
+
}
|
|
10017
10185
|
function createTestClickUpApiClient(config) {
|
|
10018
10186
|
const metadata = /* @__PURE__ */ new Map();
|
|
10019
10187
|
return {
|
|
@@ -10244,6 +10412,19 @@ function verifyClickUpSignature(rawBody, signatureHeader, secret) {
|
|
|
10244
10412
|
const wanted = Buffer.from(expected, "hex");
|
|
10245
10413
|
return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
|
|
10246
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
|
+
}
|
|
10247
10428
|
async function resyncClickUpWebhookForSignatureDrift({ rawBody, signature, config, state = {}, worktree = process.cwd(), clickupClient, saveState } = {}) {
|
|
10248
10429
|
const validation = clickUpWebhookValidationFromConfig(config);
|
|
10249
10430
|
if (!clickupClient?.listWebhooks && !clickupClient?.createWebhook) {
|
|
@@ -10450,6 +10631,105 @@ function clickUpTaskAgentMetadata(task = {}, fieldId = "") {
|
|
|
10450
10631
|
const field = fields.find((item) => String(item?.id || "") === fieldId || String(item?.name || "") === "agent_metadata");
|
|
10451
10632
|
return field?.value || "";
|
|
10452
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
|
+
}
|
|
10453
10733
|
function getNestedMetadataValue(metadata, dottedKey) {
|
|
10454
10734
|
const parts = String(dottedKey || "").split(".").filter(Boolean);
|
|
10455
10735
|
let cursor = metadata;
|
|
@@ -11355,9 +11635,9 @@ function buildClickUpWebhookAuditSummary({ method, url, config, handled, error,
|
|
|
11355
11635
|
ok: Boolean(handled?.ok),
|
|
11356
11636
|
reason: handled?.reason || result.reason || (error ? "handler_error" : void 0),
|
|
11357
11637
|
action: result.action,
|
|
11358
|
-
event: payload ? clickUpEventType(payload) : void 0,
|
|
11638
|
+
event: result.event || (payload ? clickUpEventType(payload) : void 0),
|
|
11359
11639
|
task: result.taskId || clickUpTaskIdFromPayload(payload || {}),
|
|
11360
|
-
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,
|
|
11361
11641
|
session: result.sessionId,
|
|
11362
11642
|
base_path: config?.basePath || "",
|
|
11363
11643
|
request_file: requestFile || void 0
|
|
@@ -11371,7 +11651,13 @@ function writeClickUpWebhookAuditLog({ method, url, headers = {}, rawBody = "",
|
|
|
11371
11651
|
if (level === "error" && !failed) return;
|
|
11372
11652
|
const logDir = clickUpWebhookAuditLogDir();
|
|
11373
11653
|
fs5.mkdirSync(logDir, { recursive: true });
|
|
11374
|
-
const secretValues = [
|
|
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);
|
|
11375
11661
|
let requestFile;
|
|
11376
11662
|
if (level === "verbose") {
|
|
11377
11663
|
const absoluteRequestFile = clickUpWebhookRequestFilePath(at, logDir);
|
|
@@ -11833,6 +12119,176 @@ function runClickUpWebhookRouteInBackground({ payload, config, state, worktree,
|
|
|
11833
12119
|
});
|
|
11834
12120
|
return route;
|
|
11835
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
|
+
}
|
|
11836
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 } = {}) {
|
|
11837
12293
|
let payload = null;
|
|
11838
12294
|
let handled = null;
|
|
@@ -11895,13 +12351,17 @@ function clickUpListenerFingerprint({ config, state, worktree, clickupClient, op
|
|
|
11895
12351
|
teamId: config?.teamId || "",
|
|
11896
12352
|
publicUrl: config?.webhook?.publicUrl || "",
|
|
11897
12353
|
path: clickUpWebhookExpectedPath(config),
|
|
12354
|
+
githubPublicUrl: config?.github?.webhook?.publicUrl || "",
|
|
12355
|
+
githubPath: gitHubWebhookExpectedPath(config),
|
|
12356
|
+
githubEnabled: gitHubWebhookEnabled(config),
|
|
12357
|
+
githubEvents: config?.github?.webhook?.events || [],
|
|
11898
12358
|
location: config?.webhook?.location || {},
|
|
11899
12359
|
webhookId: state?.webhookId || "",
|
|
11900
12360
|
secret: state?.secret || "",
|
|
11901
12361
|
events: config?.webhook?.events || []
|
|
11902
12362
|
});
|
|
11903
12363
|
}
|
|
11904
|
-
function startClickUpWebhookListener({ config, state, worktree, clickupClient, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
|
|
12364
|
+
function startClickUpWebhookListener({ config, state, worktree, clickupClient, githubClient = null, openCodeClient, listenerRegistry = activeClickUpWebhookListeners } = {}) {
|
|
11905
12365
|
if (!isClickUpWebhookStateActive(state, config)) return { active: false, ready: false, reason: "inactive_state" };
|
|
11906
12366
|
const key = clickUpListenerKey(config);
|
|
11907
12367
|
const fingerprint = clickUpListenerFingerprint({ config, state, worktree, clickupClient, openCodeClient });
|
|
@@ -11920,7 +12380,9 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
|
|
|
11920
12380
|
return;
|
|
11921
12381
|
}
|
|
11922
12382
|
const requestPath = new URL(req.url || "/", config.webhook.publicUrl).pathname;
|
|
11923
|
-
|
|
12383
|
+
const isClickUpPath = requestPath === clickUpWebhookExpectedPath(config);
|
|
12384
|
+
const isGitHubPath = gitHubWebhookEnabled(config) && requestPath === gitHubWebhookExpectedPath(config);
|
|
12385
|
+
if (!isClickUpPath && !isGitHubPath) {
|
|
11924
12386
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
11925
12387
|
res.end(JSON.stringify({ ok: false, reason: "wrong_path" }));
|
|
11926
12388
|
return;
|
|
@@ -11947,7 +12409,7 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
|
|
|
11947
12409
|
if (overflow || res.writableEnded) return;
|
|
11948
12410
|
const latestState = readClickUpWebhookState(worktree, config);
|
|
11949
12411
|
try {
|
|
11950
|
-
const
|
|
12412
|
+
const common = {
|
|
11951
12413
|
method: req.method,
|
|
11952
12414
|
url: req.url,
|
|
11953
12415
|
headers: req.headers,
|
|
@@ -11959,7 +12421,8 @@ function startClickUpWebhookListener({ config, state, worktree, clickupClient, o
|
|
|
11959
12421
|
openCodeClient,
|
|
11960
12422
|
saveState: (nextState) => writeClickUpWebhookState(worktree, nextState, config),
|
|
11961
12423
|
asyncRouting: true
|
|
11962
|
-
}
|
|
12424
|
+
};
|
|
12425
|
+
const handled = isGitHubPath ? await handleGitHubWebhookRequest({ ...common, githubClient }) : await handleClickUpWebhookRequest(common);
|
|
11963
12426
|
res.writeHead(handled.status, { "Content-Type": "application/json" });
|
|
11964
12427
|
res.end(JSON.stringify({ ok: handled.ok, reason: handled.reason, result: handled.result?.action }));
|
|
11965
12428
|
} catch (error) {
|
|
@@ -13077,6 +13540,7 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
13077
13540
|
const operatingTeamMode = getOperatingTeamMode(repoCfg);
|
|
13078
13541
|
const clickUpWebhookValidation = normalizeClickUpWebhookConfig(pickConfiguredClickUp(input, repoCfg, resolvedPluginOptions), worktree);
|
|
13079
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);
|
|
13080
13544
|
let clickUpWebhookRuntime = { active: false, valid: false, reason: "not_configured" };
|
|
13081
13545
|
if (clickUpWebhookValidation.complete) {
|
|
13082
13546
|
try {
|
|
@@ -13101,6 +13565,7 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
13101
13565
|
state: listenerState,
|
|
13102
13566
|
worktree,
|
|
13103
13567
|
clickupClient: lifecycleClickUpClient,
|
|
13568
|
+
githubClient: runtimeGitHubClient,
|
|
13104
13569
|
openCodeClient: input.client,
|
|
13105
13570
|
listenerRegistry
|
|
13106
13571
|
});
|
|
@@ -13776,7 +14241,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
|
|
|
13776
14241
|
}
|
|
13777
14242
|
};
|
|
13778
14243
|
}
|
|
13779
|
-
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 };
|
|
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 };
|
|
13780
14245
|
export {
|
|
13781
14246
|
OptimaPlugin as default
|
|
13782
14247
|
};
|