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