@defend-tech/opencode-optima 0.1.60 → 0.1.62

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 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 plus remove the PM assignee before assigning `CTO`/`PO` at plan end, `in progress` execute, `validation` Tech Lead + Validator/QA gates. After validation, Validator/QA may merge validated subtasks into the parent branch without `CTO`/`PO` approval; validated parent tasks stay in `validation` for `CTO`/`PO` approval, they approve by moving the parent to `merge`, and Validator/QA then attempts the parent merge into `dev`. `completed`/`Closed` ignore unless reopened.
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.
@@ -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 plus remove the PM assignee before assigning `CTO`/`PO` at plan end, `in progress` execute, `validation` Tech Lead + Validator/QA gates. Validator/QA may merge validated subtasks into the parent branch without `CTO`/`PO` approval; validated parent tasks stay in `validation` for `CTO`/`PO` approval, they approve by moving the parent to `merge`, and Validator/QA then attempts the parent merge into `dev`. `completed`/`Closed` ignore unless reopened.
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 `CTO` plus `PO` or the next owner.
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
 
@@ -40,11 +40,12 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
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 `CTO` + `PO` or next owner; target zero PM-assigned tasks.
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 parents stay in `validation`, assigned to `CTO`/`PO`, ready for approval.
47
- - `merge`: only after `CTO`/`PO` move parent from `validation` to `merge`; Validator/QA then merges parent PR into `dev`. Conflicts or merge failures return the affected item to `in progress`.
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
@@ -52,24 +53,24 @@ You are Workflow_Product_Manager, Optima's ClickUp-first delivery orchestrator.
52
53
  - Principal workspace stays on `dev`; never use `main` for delivery and never push directly to `main`.
53
54
  - Do not implement, plan, or write ClickUp task mirrors in the principal workspace. Use task-specific worktrees/branches.
54
55
  - To plan a ClickUp task, first create or reuse that task's branch/worktree with `optima_clickup_start_task`; webhook-created worktrees must be provisioned or registered through the configured OpenChamber Git API (`clickup.openchamber.base_url` `/api/git/worktrees`) before local `.optima` mirror writes.
55
- - Use `clickup.opencode.base_url` for legacy OpenCode session/prompt delivery, not as the preferred worktree creation API when `clickup.openchamber.base_url` is configured.
56
+ - Use `clickup.openchamber.base_url` only for OpenChamber Git worktree API calls; use `clickup.opencode.base_url` for OpenCode workspace/project sync, visibility checks, and session/prompt delivery.
56
57
  - If OpenChamber cannot create or verify the required branch/worktree (especially subtask start-from-parent semantics), fail closed with a ClickUp blocker comment instead of silently using raw `git worktree add`.
57
58
  - Do not update the principal `dev` workspace `.optima/tasks/current.md` for ClickUp task planning.
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 `CTO`/`PO` move to `merge`; release -> `dev` to `main` only after explicit approval.
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 `CTO` plus `PO` or next owner.
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 `CTO`/`PO` or any next owner, remove the PM assignee and verify no task remains assigned to Workflow/Product Manager unless explicitly re-queued.
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
@@ -8527,6 +8527,7 @@ var CLICKUP_WEBHOOK_REQUEST_TIMEOUT_MS = 1e4;
8527
8527
  var CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT = 50;
8528
8528
  var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
8529
8529
  var CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS = 3e4;
8530
+ var CLICKUP_WORKTREE_FAILURE_COMMENT_DEDUPE_MS = 10 * 60 * 1e3;
8530
8531
  var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
8531
8532
  var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
8532
8533
  var DISCUSSION_BACKFILL_FETCH_LIMIT = 100;
@@ -8844,9 +8845,9 @@ var CLICKUP_REQUIRED_SUMMARY_SECTIONS = [
8844
8845
  ];
8845
8846
  var CLICKUP_RAW_LOG_SECTION_NAMES = /* @__PURE__ */ new Set(["Raw Logs", "Logs", "Full Logs", "Command Output", "Transcript"]);
8846
8847
  var CLICKUP_TRANSITIONS = /* @__PURE__ */ new Map([
8847
- ["plan->in progress", { status: "in progress", assignFinalApprovers: true, comment: "Plan complete; assigning CTO+PO for visibility and moving to implementation." }],
8848
+ ["plan->in progress", { status: "in progress", comment: "Plan complete; moving to implementation without generic CTO/PO assignment." }],
8848
8849
  ["in progress->validation", { status: "validation", comment: "Implementation complete; ready for validation." }],
8849
- ["validation->merge", { status: "merge", assignFinalApprovers: true, comment: "Validation passed; parent task is ready for CTO+PO merge approval." }],
8850
+ ["validation->merge", { status: "merge", assignFinalApprovers: true, parentOnlyFinalApproval: true, comment: "Parent validation passed with a functional preview URL; ready for CTO/PO approval flow." }],
8850
8851
  ["validation->in progress", { status: "in progress", comment: "Validation failed; returning to implementation." }],
8851
8852
  ["merge->completed", { status: "completed", comment: "Merge complete; closing delivery task." }],
8852
8853
  ["completed->in progress", { status: "in progress", comment: "Task reopened; returning to implementation." }],
@@ -9234,30 +9235,36 @@ function openChamberUrl(baseUrl, pathname, query = {}) {
9234
9235
  }
9235
9236
  return url.toString();
9236
9237
  }
9237
- async function readOpenChamberJson(response, endpoint) {
9238
+ async function readOpenChamberJson(response, endpoint, serviceName = "OpenChamber") {
9238
9239
  const text = await response.text();
9239
9240
  let data = null;
9240
9241
  if (text.trim()) {
9241
9242
  try {
9242
9243
  data = JSON.parse(text);
9243
9244
  } catch {
9244
- throw new Error(`OpenChamber ${endpoint} returned non-JSON response.`);
9245
+ throw new Error(`${serviceName} ${endpoint} returned non-JSON response.`);
9245
9246
  }
9246
9247
  }
9247
9248
  if (!response.ok) {
9248
9249
  const message = data?.error?.message || data?.message || data?.data?.message || text.slice(0, 200) || `HTTP ${response.status}`;
9249
- throw new Error(`OpenChamber ${endpoint} failed: ${response.status} ${message}`);
9250
+ throw new Error(`${serviceName} ${endpoint} failed: ${response.status} ${message}`);
9250
9251
  }
9251
9252
  return data;
9252
9253
  }
9253
- async function requestOpenChamberJson({ baseUrl, endpoint, method = "GET", directory, body, fetchImpl = globalThis.fetch } = {}) {
9254
- if (typeof fetchImpl !== "function") throw new Error("OpenChamber worktree provisioning requires fetch.");
9254
+ async function requestServiceJson({ baseUrl, serviceName = "OpenChamber", endpoint, method = "GET", directory, body, fetchImpl = globalThis.fetch } = {}) {
9255
+ if (typeof fetchImpl !== "function") throw new Error(`${serviceName} API calls require fetch.`);
9255
9256
  const response = await fetchImpl(openChamberUrl(baseUrl, endpoint, { directory }), {
9256
9257
  method,
9257
9258
  headers: body ? { "content-type": "application/json" } : void 0,
9258
9259
  body: body ? JSON.stringify(body) : void 0
9259
9260
  });
9260
- return readOpenChamberJson(response, endpoint);
9261
+ return readOpenChamberJson(response, endpoint, serviceName);
9262
+ }
9263
+ async function requestOpenChamberJson(options = {}) {
9264
+ return requestServiceJson({ ...options, serviceName: "OpenChamber" });
9265
+ }
9266
+ async function requestOpenCodeJson(options = {}) {
9267
+ return requestServiceJson({ ...options, serviceName: "OpenCode" });
9261
9268
  }
9262
9269
  function normalizeOpenChamberCollection(value) {
9263
9270
  if (Array.isArray(value)) return value;
@@ -9296,39 +9303,40 @@ function openChamberListIncludesBranch(list, directory, branch) {
9296
9303
  return !entryBranch || entryBranch === branch;
9297
9304
  });
9298
9305
  }
9299
- async function findOpenChamberProject({ baseUrl, baseWorktree, fetchImpl = globalThis.fetch } = {}) {
9300
- const projects = await requestOpenChamberJson({ baseUrl, endpoint: "/project", directory: baseWorktree, fetchImpl });
9306
+ async function findOpenChamberProject({ opencodeBaseUrl, baseWorktree, fetchImpl = globalThis.fetch } = {}) {
9307
+ const projects = await requestOpenCodeJson({ baseUrl: opencodeBaseUrl, endpoint: "/project", directory: baseWorktree, fetchImpl });
9301
9308
  if (!Array.isArray(projects)) return null;
9302
9309
  const resolvedBase = path5.resolve(baseWorktree);
9303
9310
  return projects.find((project) => path5.resolve(String(project?.worktree || project?.path || "")) === resolvedBase) || null;
9304
9311
  }
9305
- async function refreshOpenChamberProjectCopy({ baseUrl, baseWorktree, projectId, fetchImpl = globalThis.fetch } = {}) {
9312
+ async function refreshOpenChamberProjectCopy({ opencodeBaseUrl, projectId, fetchImpl = globalThis.fetch } = {}) {
9306
9313
  if (!projectId) return { refreshed: false, reason: "project_id_unavailable" };
9307
- const response = await fetchImpl(openChamberUrl(baseUrl, `/experimental/project/${encodeURIComponent(projectId)}/copy/refresh`, {}), { method: "POST" });
9308
- await readOpenChamberJson(response, "/experimental/project/{projectID}/copy/refresh");
9314
+ await requestOpenCodeJson({ baseUrl: opencodeBaseUrl, endpoint: `/experimental/project/${encodeURIComponent(projectId)}/copy/refresh`, method: "POST", fetchImpl });
9309
9315
  return { refreshed: true, projectId };
9310
9316
  }
9311
- async function listOpenChamberGitWorktrees({ baseUrl, baseWorktree, fetchImpl = globalThis.fetch } = {}) {
9312
- return requestOpenChamberJson({ baseUrl, endpoint: "/api/git/worktrees", directory: baseWorktree, fetchImpl });
9317
+ async function listOpenChamberGitWorktrees({ openchamberBaseUrl, baseUrl, baseWorktree, fetchImpl = globalThis.fetch } = {}) {
9318
+ return requestOpenChamberJson({ baseUrl: openchamberBaseUrl || baseUrl, endpoint: "/api/git/worktrees", directory: baseWorktree, fetchImpl });
9313
9319
  }
9314
- async function verifyOpenChamberGitWorktree({ baseUrl, baseWorktree, worktreePath, branch, fetchImpl = globalThis.fetch } = {}) {
9315
- const worktrees = await listOpenChamberGitWorktrees({ baseUrl, baseWorktree, fetchImpl });
9320
+ async function verifyOpenChamberGitWorktree({ openchamberBaseUrl, baseUrl, baseWorktree, worktreePath, branch, fetchImpl = globalThis.fetch } = {}) {
9321
+ const worktrees = await listOpenChamberGitWorktrees({ openchamberBaseUrl: openchamberBaseUrl || baseUrl, baseWorktree, fetchImpl });
9316
9322
  const verified = openChamberListIncludesBranch(worktrees, worktreePath, branch);
9317
9323
  if (!verified) {
9318
9324
  throw new Error(`OpenChamber Git worktree verification failed for ${worktreePath} on ${branch}.`);
9319
9325
  }
9320
9326
  return { worktree: true, branch: true };
9321
9327
  }
9322
- async function syncOpenChamberWorktreeVisibility({ baseUrl, baseWorktree, worktreePath, branch, fetchImpl = globalThis.fetch } = {}) {
9323
- const gitWorktree = await verifyOpenChamberGitWorktree({ baseUrl, baseWorktree, worktreePath, branch, fetchImpl });
9324
- await requestOpenChamberJson({ baseUrl, endpoint: "/experimental/workspace/sync-list", method: "POST", directory: baseWorktree, fetchImpl });
9325
- const project = await findOpenChamberProject({ baseUrl, baseWorktree, fetchImpl });
9326
- if (!project?.id) throw new Error("OpenChamber project was not found after workspace sync; refusing to treat worktree as visible.");
9327
- await refreshOpenChamberProjectCopy({ baseUrl, baseWorktree, projectId: project.id, fetchImpl });
9328
+ async function syncOpenChamberWorktreeVisibility({ openchamberBaseUrl, opencodeBaseUrl, baseUrl, baseWorktree, worktreePath, branch, fetchImpl = globalThis.fetch } = {}) {
9329
+ const effectiveOpenChamberBaseUrl = openchamberBaseUrl || baseUrl;
9330
+ const effectiveOpenCodeBaseUrl = opencodeBaseUrl || baseUrl;
9331
+ const gitWorktree = await verifyOpenChamberGitWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, worktreePath, branch, fetchImpl });
9332
+ await requestOpenCodeJson({ baseUrl: effectiveOpenCodeBaseUrl, endpoint: "/experimental/workspace/sync-list", method: "POST", directory: baseWorktree, fetchImpl });
9333
+ const project = await findOpenChamberProject({ opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, fetchImpl });
9334
+ if (!project?.id) throw new Error("OpenCode project was not found after workspace sync; refusing to treat worktree as visible.");
9335
+ await refreshOpenChamberProjectCopy({ opencodeBaseUrl: effectiveOpenCodeBaseUrl, projectId: project.id, fetchImpl });
9328
9336
  const [worktrees, workspaces, directories] = await Promise.all([
9329
- listOpenChamberGitWorktrees({ baseUrl, baseWorktree, fetchImpl }),
9330
- requestOpenChamberJson({ baseUrl, endpoint: "/experimental/workspace", directory: baseWorktree, fetchImpl }),
9331
- requestOpenChamberJson({ baseUrl, endpoint: `/project/${encodeURIComponent(project.id)}/directories`, directory: baseWorktree, fetchImpl })
9337
+ listOpenChamberGitWorktrees({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, fetchImpl }),
9338
+ requestOpenCodeJson({ baseUrl: effectiveOpenCodeBaseUrl, endpoint: "/experimental/workspace", directory: baseWorktree, fetchImpl }),
9339
+ requestOpenCodeJson({ baseUrl: effectiveOpenCodeBaseUrl, endpoint: `/project/${encodeURIComponent(project.id)}/directories`, directory: baseWorktree, fetchImpl })
9332
9340
  ]);
9333
9341
  const visibility = {
9334
9342
  worktree: openChamberListIncludesBranch(worktrees, worktreePath, branch),
@@ -9338,7 +9346,7 @@ async function syncOpenChamberWorktreeVisibility({ baseUrl, baseWorktree, worktr
9338
9346
  projectId: project.id
9339
9347
  };
9340
9348
  if (!visibility.worktree || !visibility.workspace || !visibility.projectDirectory) {
9341
- throw new Error(`OpenChamber visibility verification failed for ${worktreePath}: ${JSON.stringify(visibility)}`);
9349
+ throw new Error(`OpenCode visibility verification failed for ${worktreePath}: ${JSON.stringify(visibility)}`);
9342
9350
  }
9343
9351
  return visibility;
9344
9352
  }
@@ -9347,12 +9355,13 @@ function assertOpenChamberClickUpWorktreePath({ baseWorktree, worktreePath } = {
9347
9355
  throw new Error(`OpenChamber worktree path is outside the configured ClickUp sibling scope: ${worktreePath}`);
9348
9356
  }
9349
9357
  }
9350
- async function createOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch, worktreePath, startPoint, branchExists = false, fetchImpl = globalThis.fetch } = {}) {
9358
+ async function createOpenChamberClickUpWorktree({ openchamberBaseUrl, baseUrl, baseWorktree, branch, worktreePath, startPoint, branchExists = false, fetchImpl = globalThis.fetch } = {}) {
9359
+ const effectiveOpenChamberBaseUrl = openchamberBaseUrl || baseUrl;
9351
9360
  const worktreeName = clickUpOpenChamberWorktreeName(branch);
9352
9361
  const body = { name: worktreeName, mode: branchExists ? "existing" : "new", worktreeName, branchName: branch, startRef: branchExists ? branch : startPoint };
9353
9362
  let created;
9354
9363
  try {
9355
- created = await requestOpenChamberJson({ baseUrl, endpoint: "/api/git/worktrees", method: "POST", directory: baseWorktree, body, fetchImpl });
9364
+ created = await requestOpenChamberJson({ baseUrl: effectiveOpenChamberBaseUrl, endpoint: "/api/git/worktrees", method: "POST", directory: baseWorktree, body, fetchImpl });
9356
9365
  } catch (error) {
9357
9366
  throw new Error(`OpenChamber could not create ${branch} from ${branchExists ? branch : startPoint}; API may not support required branch/startRef semantics. Fail-closed: ${error.message}`);
9358
9367
  }
@@ -9364,15 +9373,17 @@ async function createOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch,
9364
9373
  if (createdBranch !== branch) {
9365
9374
  throw new Error(`OpenChamber created unexpected branch ${createdBranch || "<unknown>"}; expected ${branch}.`);
9366
9375
  }
9367
- const verified = await verifyOpenChamberGitWorktree({ baseUrl, baseWorktree, worktreePath: createdDirectory, branch, fetchImpl });
9376
+ const verified = await verifyOpenChamberGitWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, worktreePath: createdDirectory, branch, fetchImpl });
9368
9377
  return { created, worktree: path5.resolve(createdDirectory), branch: createdBranch, verified };
9369
9378
  }
9370
- async function registerOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch, worktreePath, fetchImpl = globalThis.fetch, source = "reuse" } = {}) {
9379
+ async function registerOpenChamberClickUpWorktree({ openchamberBaseUrl, opencodeBaseUrl, baseUrl, baseWorktree, branch, worktreePath, fetchImpl = globalThis.fetch, source = "reuse" } = {}) {
9371
9380
  assertOpenChamberClickUpWorktreePath({ baseWorktree, worktreePath });
9372
- const visibility = await syncOpenChamberWorktreeVisibility({ baseUrl, baseWorktree, worktreePath, branch, fetchImpl });
9381
+ const visibility = await syncOpenChamberWorktreeVisibility({ openchamberBaseUrl: openchamberBaseUrl || baseUrl, opencodeBaseUrl: opencodeBaseUrl || baseUrl, baseWorktree, worktreePath, branch, fetchImpl });
9373
9382
  return { branch, worktree: path5.resolve(worktreePath), reused: true, provider: "openchamber", openChamber: { source, visibility } };
9374
9383
  }
9375
- async function ensureClickUpTaskWorktreeOpenChamber({ baseWorktree = "", taskId, taskType = "Tarea", parentTaskId = "", subtaskId = "", existingMetadata = {}, runGitFn = runGit, baseUrl = "", fetchImpl = globalThis.fetch, log = null } = {}) {
9384
+ async function ensureClickUpTaskWorktreeOpenChamber({ baseWorktree = "", taskId, taskType = "Tarea", parentTaskId = "", subtaskId = "", existingMetadata = {}, runGitFn = runGit, openchamberBaseUrl = "", opencodeBaseUrl = "", baseUrl = "", fetchImpl = globalThis.fetch, log = null } = {}) {
9385
+ const effectiveOpenChamberBaseUrl = openchamberBaseUrl || baseUrl;
9386
+ const effectiveOpenCodeBaseUrl = opencodeBaseUrl || baseUrl;
9376
9387
  const effectiveParent = parentTaskId || taskId;
9377
9388
  const isSubtask = isClickUpSubtaskRoute({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
9378
9389
  const parentBranch = isSubtask ? deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent }) : "";
@@ -9380,13 +9391,13 @@ async function ensureClickUpTaskWorktreeOpenChamber({ baseWorktree = "", taskId,
9380
9391
  const prTarget = parentBranch || "dev";
9381
9392
  const existing = safeExistingClickUpWorktree({ metadata: existingMetadata, branch });
9382
9393
  if (existing) {
9383
- const registered = await registerOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch, worktreePath: existing.worktree, fetchImpl, source: "metadata_reuse" });
9394
+ const registered = await registerOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, branch, worktreePath: existing.worktree, fetchImpl, source: "metadata_reuse" });
9384
9395
  log?.({ type: "openchamber_worktree_registered", taskId, branch, worktree: registered.worktree, source: "metadata_reuse", visibility: registered.openChamber.visibility });
9385
9396
  return { ...registered, parentBranch: parentBranch || void 0, prTarget };
9386
9397
  }
9387
9398
  const worktreePath = deriveClickUpWorktree({ baseWorktree, taskId, taskType, parentTaskId: effectiveParent, subtaskId });
9388
9399
  if (fs5.existsSync(worktreePath)) {
9389
- const registered = await registerOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch, worktreePath, fetchImpl, source: "existing_directory" });
9400
+ const registered = await registerOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, branch, worktreePath, fetchImpl, source: "existing_directory" });
9390
9401
  log?.({ type: "openchamber_worktree_registered", taskId, branch, worktree: registered.worktree, source: "existing_directory", visibility: registered.openChamber.visibility });
9391
9402
  return { ...registered, parentBranch: parentBranch || void 0, prTarget };
9392
9403
  }
@@ -9394,40 +9405,54 @@ async function ensureClickUpTaskWorktreeOpenChamber({ baseWorktree = "", taskId,
9394
9405
  if (isSubtask) {
9395
9406
  const parentWorktree = deriveClickUpWorktree({ baseWorktree, taskId: effectiveParent, taskType, parentTaskId: effectiveParent });
9396
9407
  if (fs5.existsSync(parentWorktree)) {
9397
- const registeredParent = await registerOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch: parentBranch, worktreePath: parentWorktree, fetchImpl, source: "parent_existing_directory" });
9408
+ const registeredParent = await registerOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, branch: parentBranch, worktreePath: parentWorktree, fetchImpl, source: "parent_existing_directory" });
9398
9409
  parentBootstrap = { branch: parentBranch, worktree: registeredParent.worktree, reused: true, provider: "openchamber", visibility: registeredParent.openChamber.visibility };
9399
9410
  } else {
9400
9411
  const parentStartPoint = resolveClickUpDevStartPoint(baseWorktree, runGitFn);
9401
9412
  const parentBranchExists = clickUpGitRefExists(baseWorktree, parentBranch, runGitFn);
9402
- const createdParent = await createOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch: parentBranch, worktreePath: parentWorktree, startPoint: parentStartPoint, branchExists: parentBranchExists, fetchImpl });
9403
- const parentVisibility = await syncOpenChamberWorktreeVisibility({ baseUrl, baseWorktree, worktreePath: createdParent.worktree, branch: parentBranch, fetchImpl });
9413
+ const createdParent = await createOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, branch: parentBranch, worktreePath: parentWorktree, startPoint: parentStartPoint, branchExists: parentBranchExists, fetchImpl });
9414
+ const parentVisibility = await syncOpenChamberWorktreeVisibility({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, worktreePath: createdParent.worktree, branch: parentBranch, fetchImpl });
9404
9415
  parentBootstrap = { branch: parentBranch, worktree: createdParent.worktree, startPoint: parentBranchExists ? parentBranch : parentStartPoint, reused: parentBranchExists, provider: "openchamber", visibility: parentVisibility };
9405
9416
  log?.({ type: "openchamber_worktree_created", taskId: effectiveParent, branch: parentBranch, worktree: parentBootstrap.worktree, startPoint: parentBootstrap.startPoint, visibility: parentVisibility });
9406
9417
  }
9407
9418
  }
9408
9419
  const startPoint = isSubtask ? parentBranch : resolveClickUpDevStartPoint(baseWorktree, runGitFn);
9409
9420
  const branchExists = clickUpGitRefExists(baseWorktree, branch, runGitFn);
9410
- const created = await createOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch, worktreePath, startPoint, branchExists, fetchImpl });
9411
- const visibility = await syncOpenChamberWorktreeVisibility({ baseUrl, baseWorktree, worktreePath: created.worktree, branch, fetchImpl });
9421
+ const created = await createOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, branch, worktreePath, startPoint, branchExists, fetchImpl });
9422
+ const visibility = await syncOpenChamberWorktreeVisibility({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, worktreePath: created.worktree, branch, fetchImpl });
9412
9423
  log?.({ type: "openchamber_worktree_created", taskId, branch, worktree: created.worktree, startPoint: branchExists ? branch : startPoint, visibility });
9413
9424
  return { branch, worktree: created.worktree, reused: false, startPoint, parentBranch: parentBranch || void 0, prTarget, parentBootstrap: parentBootstrap || void 0, provider: "openchamber", openChamber: { source: "created", visibility } };
9414
9425
  }
9415
9426
  async function ensureClickUpTaskWorktreeForWebhook({ opencodeBaseUrl = "", opencodeBaseUrlConfigured = false, openchamberBaseUrl = "", openchamberBaseUrlConfigured = false, clickupClient = null, webhookWorktree = process.cwd(), fetchImpl = globalThis.fetch, ...options } = {}) {
9416
- void opencodeBaseUrl;
9417
9427
  void opencodeBaseUrlConfigured;
9418
9428
  if (!openchamberBaseUrlConfigured || !openchamberBaseUrl) return ensureClickUpTaskWorktree(options);
9419
9429
  try {
9420
- return await ensureClickUpTaskWorktreeOpenChamber({ ...options, baseUrl: openchamberBaseUrl, fetchImpl, log: (entry) => appendClickUpWebhookLocalLog(webhookWorktree, entry) });
9430
+ return await ensureClickUpTaskWorktreeOpenChamber({ ...options, openchamberBaseUrl, opencodeBaseUrl, fetchImpl, log: (entry) => appendClickUpWebhookLocalLog(webhookWorktree, entry) });
9421
9431
  } catch (error) {
9422
9432
  const message = `OpenChamber worktree provisioning failed for ${options.taskId || "unknown task"}; raw git fallback is disabled to avoid invisible worktrees. ${error.message}`;
9423
9433
  appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_failed", taskId: options.taskId || null, message });
9424
9434
  if (typeof clickupClient?.postTaskComment === "function") {
9425
- try {
9426
- await clickupClient.postTaskComment({ taskId: options.taskId, comment: `${message}
9435
+ const taskId = options.taskId || "unknown task";
9436
+ const ledgerPath = clickUpCommentLedgerPath(webhookWorktree);
9437
+ const comment = `${message}
9427
9438
 
9428
- Optima did not run raw git worktree fallback. Please verify clickup.openchamber.base_url and OpenChamber Git API compatibility.` });
9429
- } catch (commentError) {
9430
- appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_blocker_comment_failed", taskId: options.taskId || null, message: commentError.message });
9439
+ Optima did not run raw git worktree fallback. Please verify clickup.openchamber.base_url and OpenCode/OpenChamber endpoint configuration.`;
9440
+ let dedupe = { post: true, key: clickUpWorktreeFailureCommentKey({ taskId, message }) };
9441
+ try {
9442
+ dedupe = shouldPostClickUpWorktreeFailureComment({ ledgerPath, taskId, message });
9443
+ } catch (ledgerError) {
9444
+ appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_comment_dedupe_failed", taskId, message: ledgerError.message });
9445
+ }
9446
+ if (dedupe.post) {
9447
+ try {
9448
+ await clickupClient.postTaskComment({ taskId: options.taskId, comment });
9449
+ recordClickUpWorktreeFailureComment({ ledgerPath, taskId, message, posted: true });
9450
+ } catch (commentError) {
9451
+ appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_blocker_comment_failed", taskId: options.taskId || null, message: commentError.message });
9452
+ }
9453
+ } else {
9454
+ appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_blocker_comment_deduped", taskId, key: dedupe.key, reason: dedupe.reason });
9455
+ recordClickUpWorktreeFailureComment({ ledgerPath, taskId, message, posted: false, skippedReason: dedupe.reason });
9431
9456
  }
9432
9457
  }
9433
9458
  throw new Error(message);
@@ -9561,11 +9586,14 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9561
9586
  const key = `${from}->${to}`;
9562
9587
  const rule = CLICKUP_TRANSITIONS.get(key);
9563
9588
  if (!rule) return { ok: false, dryRun: true, message: `Transition not allowed: ${key}` };
9564
- const assignsFinalApprovers = rule.assignFinalApprovers === true;
9565
- const requiresPlanCompletionContract = from === "plan" && assignsFinalApprovers;
9589
+ const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && isSubtask);
9590
+ const requiresPlanCompletionContract = from === "plan" && to === "in progress";
9566
9591
  const definitionContent = compactMarkdownValue(definition);
9567
9592
  const description = compactMarkdownValue(planDescription);
9568
9593
  const validationErrors = [];
9594
+ if (rule.parentOnlyFinalApproval && isSubtask) {
9595
+ validationErrors.push("Subtasks must not use the parent final-approval transition; merge validated subtasks directly into the parent branch/workspace.");
9596
+ }
9569
9597
  if (assignsFinalApprovers && !isRealClickUpAssigneeId(productManagerAssignee) && requireProductManagerAssignee !== false) {
9570
9598
  validationErrors.push("productManagerAssignee must be the configured ClickUp PM assignee ID before assigning final approvers.");
9571
9599
  }
@@ -9574,10 +9602,10 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9574
9602
  if (!definitionContent) validationErrors.push("definition is required and must contain the complete plan/Definition content at plan completion.");
9575
9603
  }
9576
9604
  if (validationErrors.length > 0) return clickUpPayloadValidationError(validationErrors);
9577
- const finalApprovers = rule.assignFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
9605
+ const finalApprovers = assignsFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
9578
9606
  const explicitRemovals = (removeAssignees || []).filter(Boolean);
9579
9607
  const normalizedProductManagerAssignee = isRealClickUpAssigneeId(productManagerAssignee) ? String(productManagerAssignee).trim() : "";
9580
- const removalTargets = rule.assignFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
9608
+ const removalTargets = assignsFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
9581
9609
  const assigned = [...new Set([...assignees || [], ...finalApprovers].filter(Boolean))].filter((assignee) => !removalTargets.includes(assignee));
9582
9610
  const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
9583
9611
  const fields = authority ? { merge_authority: JSON.stringify(authority) } : {};
@@ -9592,7 +9620,7 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
9592
9620
  assignment_delta: {
9593
9621
  add: assigned,
9594
9622
  remove: removalTargets,
9595
- objective: rule.assignFinalApprovers ? "zero_product_manager_assigned_tasks" : "preserve_existing_owner_policy"
9623
+ objective: assignsFinalApprovers ? "zero_product_manager_assigned_tasks" : "preserve_existing_owner_policy"
9596
9624
  },
9597
9625
  comment: rule.comment,
9598
9626
  description,
@@ -10050,11 +10078,17 @@ function createTestClickUpApiClient(config) {
10050
10078
  __metadata: metadata
10051
10079
  };
10052
10080
  }
10053
- function normalizeClickUpWebhookApiResponse(response = {}, fallbackConfig = null) {
10054
- const webhook = response?.webhook || response?.data || response || {};
10081
+ function clickUpWebhookRemoteInactiveReason(webhook = {}) {
10055
10082
  const healthStatus = String(webhook.health?.status || webhook.health_status || "").trim().toLowerCase();
10056
10083
  const status = String(webhook.status || webhook.state || "").trim().toLowerCase();
10057
- const active = webhook.active !== false && !["inactive", "disabled", "suspended", "failed", "failing", "error", "errored", "paused"].includes(status) && !["failed", "failing", "unhealthy", "error", "errored", "suspended", "paused"].includes(healthStatus);
10084
+ if (["suspended", "paused", "failed", "failing", "unhealthy", "error", "errored"].includes(healthStatus)) return `remote_health_${healthStatus}`;
10085
+ if (["inactive", "disabled", "suspended", "failed", "failing", "error", "errored", "paused"].includes(status)) return `remote_status_${status}`;
10086
+ if (webhook.active === false) return "remote_inactive";
10087
+ return "";
10088
+ }
10089
+ function normalizeClickUpWebhookApiResponse(response = {}, fallbackConfig = null) {
10090
+ const webhook = response?.webhook || response?.data || response || {};
10091
+ const active = !clickUpWebhookRemoteInactiveReason(webhook);
10058
10092
  return sanitizeClickUpWebhookState({
10059
10093
  active,
10060
10094
  webhookId: webhook.id || webhook.webhook_id || webhook.webhookId,
@@ -10080,20 +10114,44 @@ function clickUpWebhookLocationCompatible(webhook = {}, config = {}) {
10080
10114
  }
10081
10115
  return true;
10082
10116
  }
10117
+ function clickUpWebhookEventsCompatible(webhook = {}, config = {}) {
10118
+ if (!Array.isArray(webhook.events) || webhook.events.length === 0) return false;
10119
+ const actualEvents = new Set(webhook.events);
10120
+ return [...new Set(config?.webhook?.events || [])].every((event) => actualEvents.has(event));
10121
+ }
10122
+ function clickUpWebhookRemoteCompatibilityReason(webhook = {}, config = {}) {
10123
+ if (!clickUpWebhookLocationCompatible(webhook, config)) return "remote_location_mismatch";
10124
+ if (!clickUpWebhookEventsCompatible(webhook, config)) return "remote_events_mismatch";
10125
+ return "";
10126
+ }
10127
+ function isClickUpWebhookRemoteSelfHealableReason(reason = "") {
10128
+ const normalized = String(reason || "");
10129
+ return normalized === "remote_inactive" || normalized.startsWith("remote_health_") || normalized.startsWith("remote_status_");
10130
+ }
10083
10131
  async function findReusableClickUpWebhook(config, clickupClient = null, existingState = null) {
10084
10132
  if (!clickupClient?.listWebhooks) return null;
10085
10133
  const listed = await clickupClient.listWebhooks({ teamId: config.teamId });
10086
- let secretlessMatch = null;
10134
+ let incompatibleMatch = null;
10087
10135
  for (const webhook of clickUpWebhookListItems(listed)) {
10088
10136
  const remote = normalizeClickUpWebhookApiResponse(webhook, config);
10089
10137
  if (remote.publicUrl !== config.webhook.publicUrl) continue;
10090
- if (!clickUpWebhookLocationCompatible(webhook, config)) continue;
10138
+ const compatibilityReason = clickUpWebhookRemoteCompatibilityReason(webhook, config);
10139
+ if (compatibilityReason) {
10140
+ if (compatibilityReason === "remote_events_mismatch" && remote.webhookId && !incompatibleMatch) incompatibleMatch = { ...remote, active: false, reason: compatibilityReason };
10141
+ continue;
10142
+ }
10091
10143
  const existingSecret = existingState?.webhookId === remote.webhookId ? existingState.secret : "";
10092
10144
  const reusable = remote.secret ? remote : { ...remote, secret: existingSecret };
10093
10145
  if (isClickUpWebhookStateActive(reusable, config)) return reusable;
10094
- if (remote.webhookId && !secretlessMatch) secretlessMatch = { ...remote, active: false, reason: "remote_secret_unavailable" };
10146
+ if (remote.webhookId && !incompatibleMatch) {
10147
+ incompatibleMatch = {
10148
+ ...remote,
10149
+ active: false,
10150
+ reason: remote.active === false ? clickUpWebhookRemoteInactiveReason(webhook) || "remote_inactive" : "remote_secret_unavailable"
10151
+ };
10152
+ }
10095
10153
  }
10096
- return secretlessMatch;
10154
+ return incompatibleMatch;
10097
10155
  }
10098
10156
  async function validateClickUpWebhookState(state, config, clickupClient = null, { allowRemoteUnhealthyLocalRecovery = false } = {}) {
10099
10157
  if (!isClickUpWebhookStateActive(state, config)) return { valid: false, reason: "state_incomplete" };
@@ -10108,7 +10166,7 @@ async function validateClickUpWebhookState(state, config, clickupClient = null,
10108
10166
  }
10109
10167
  const localStateValidation = await validateClickUpWebhookState(state, config, null);
10110
10168
  if (allowRemoteUnhealthyLocalRecovery && localStateValidation.valid && remote.webhookId === state.webhookId) {
10111
- const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && [...new Set(config.webhook.events || [])].every((event) => new Set(remote.events || []).has(event));
10169
+ const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && !clickUpWebhookRemoteCompatibilityReason(match, config);
10112
10170
  if (remoteConfigMatches) {
10113
10171
  return {
10114
10172
  ...localStateValidation,
@@ -10133,12 +10191,18 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
10133
10191
  const { config } = validation;
10134
10192
  const existing = existingState ? sanitizeClickUpWebhookState(existingState, config) : readClickUpWebhookState(worktree, config);
10135
10193
  const existingValidation = await validateClickUpWebhookState(existing, config, clickupClient, { allowRemoteUnhealthyLocalRecovery: true });
10136
- if (existingValidation.valid) {
10194
+ const canReplaceUnhealthyExisting = existingValidation.mode === "local_state_remote_unhealthy" && existingValidation.remote?.webhookId && clickupClient?.deleteWebhook && clickupClient?.createWebhook;
10195
+ if (existingValidation.valid && !canReplaceUnhealthyExisting) {
10137
10196
  const state = writeClickUpWebhookState(worktree, { ...existingValidation.state, recentEventKeys: existing.recentEventKeys || [] }, config);
10138
10197
  clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: existingValidation.mode });
10139
10198
  return { active: true, valid: true, mode: existingValidation.mode, limitation: existingValidation.limitation, state };
10140
10199
  }
10141
- const reusableRemote = await findReusableClickUpWebhook(config, clickupClient, existing);
10200
+ const reusableRemote = canReplaceUnhealthyExisting ? {
10201
+ ...existingValidation.remote,
10202
+ active: false,
10203
+ secret: existingValidation.remote.secret || existing.secret,
10204
+ reason: clickUpWebhookRemoteInactiveReason(existingValidation.remote) || existingValidation.reason || "remote_unhealthy_self_heal"
10205
+ } : await findReusableClickUpWebhook(config, clickupClient, existing);
10142
10206
  if (reusableRemote) {
10143
10207
  if (isClickUpWebhookStateActive(reusableRemote, config)) {
10144
10208
  const state = writeClickUpWebhookState(worktree, { ...reusableRemote, recentEventKeys: existing.recentEventKeys || [] }, config);
@@ -10146,10 +10210,31 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
10146
10210
  clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: mode2 });
10147
10211
  return { active: true, valid: true, mode: mode2, state };
10148
10212
  }
10149
- clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_webhook_exists_without_secret" });
10150
- return { active: false, valid: false, reason: reusableRemote.reason || "remote_webhook_exists_without_secret", state: existing, remote: reusableRemote };
10213
+ if (reusableRemote.active === false && reusableRemote.reason !== "remote_secret_unavailable") {
10214
+ if (!isClickUpWebhookRemoteSelfHealableReason(reusableRemote.reason)) {
10215
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_incompatible" });
10216
+ return { active: false, valid: false, reason: reusableRemote.reason || "remote_incompatible", state: existing, remote: reusableRemote };
10217
+ }
10218
+ if (clickupClient?.deleteWebhook && clickupClient?.createWebhook) {
10219
+ const deleteResult = await deleteClickUpWebhookBestEffort({ webhookId: reusableRemote.webhookId, clickupClient, worktree, reason: reusableRemote.reason || "remote_unhealthy_self_heal" });
10220
+ if (deleteResult.ok) {
10221
+ if (existing.webhookId === reusableRemote.webhookId) markClickUpWebhookInactive(worktree, existing, config);
10222
+ } else {
10223
+ const reason = deleteResult.reason || "remote_unhealthy_delete_failed";
10224
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason });
10225
+ return { active: false, valid: false, reason, state: existing, remote: reusableRemote, deleteResult };
10226
+ }
10227
+ } else {
10228
+ const reason = clickupClient?.deleteWebhook ? "remote_unhealthy_create_unavailable" : "remote_unhealthy_delete_unavailable";
10229
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason });
10230
+ return { active: false, valid: false, reason, state: existing, remote: reusableRemote };
10231
+ }
10232
+ } else {
10233
+ clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_webhook_exists_without_secret" });
10234
+ return { active: false, valid: false, reason: reusableRemote.reason || "remote_webhook_exists_without_secret", state: existing, remote: reusableRemote };
10235
+ }
10151
10236
  }
10152
- if (existing.webhookId && clickupClient?.deleteWebhook) {
10237
+ if (existing.webhookId && existing.webhookId !== reusableRemote?.webhookId && clickupClient?.deleteWebhook) {
10153
10238
  await deleteClickUpWebhookBestEffort({ webhookId: existing.webhookId, clickupClient, worktree, reason: existingValidation.reason || "startup_self_heal" });
10154
10239
  markClickUpWebhookInactive(worktree, existing, config);
10155
10240
  }
@@ -10227,23 +10312,29 @@ function rememberClickUpWebhookEvent(state = {}, eventKey, limit = 200) {
10227
10312
  if (recent.includes(eventKey)) return { duplicate: true, state };
10228
10313
  return { duplicate: false, state: { ...state, recentEventKeys: [...recent, eventKey].slice(-limit) } };
10229
10314
  }
10230
- function readClickUpCommentLedger(ledgerPath) {
10231
- const processed = /* @__PURE__ */ new Set();
10232
- if (!ledgerPath || !fs5.existsSync(ledgerPath)) return processed;
10315
+ function readClickUpCommentLedgerEntries(ledgerPath) {
10316
+ if (!ledgerPath || !fs5.existsSync(ledgerPath)) return [];
10233
10317
  try {
10234
10318
  const raw = fs5.readFileSync(ledgerPath, "utf8");
10319
+ const entries = [];
10235
10320
  for (const [index, line] of raw.split(/\r?\n/).entries()) {
10236
10321
  if (!line.trim()) continue;
10237
10322
  try {
10238
- const entry = JSON.parse(line);
10239
- if (entry?.key) processed.add(String(entry.key));
10323
+ entries.push(JSON.parse(line));
10240
10324
  } catch (error) {
10241
10325
  throw new Error(`malformed ledger row ${index + 1}: ${error.message}`);
10242
10326
  }
10243
10327
  }
10328
+ return entries;
10244
10329
  } catch (error) {
10245
10330
  throw new Error(`ClickUp comment ledger unavailable: ${error.message}`);
10246
10331
  }
10332
+ }
10333
+ function readClickUpCommentLedger(ledgerPath) {
10334
+ const processed = /* @__PURE__ */ new Set();
10335
+ for (const entry of readClickUpCommentLedgerEntries(ledgerPath)) {
10336
+ if (entry?.key) processed.add(String(entry.key));
10337
+ }
10247
10338
  return processed;
10248
10339
  }
10249
10340
  function appendClickUpCommentLedgerEntry(ledgerPath, entry = {}) {
@@ -10309,6 +10400,35 @@ function recordClickUpCommentVersionProcessed({ ledgerPath, key, taskId, eventTy
10309
10400
  recordedAt: at.toISOString()
10310
10401
  });
10311
10402
  }
10403
+ function clickUpWorktreeFailureCommentKey({ taskId, message } = {}) {
10404
+ const hash = crypto.createHash("sha256").update(String(message || "")).digest("hex").slice(0, 16);
10405
+ return [String(taskId || "unknown").trim() || "unknown", "worktree_failure", hash].join(":");
10406
+ }
10407
+ function shouldPostClickUpWorktreeFailureComment({ ledgerPath, taskId, message, now = /* @__PURE__ */ new Date(), windowMs = CLICKUP_WORKTREE_FAILURE_COMMENT_DEDUPE_MS } = {}) {
10408
+ const key = clickUpWorktreeFailureCommentKey({ taskId, message });
10409
+ const nowMs = now instanceof Date ? now.getTime() : new Date(now).getTime();
10410
+ for (const entry of readClickUpCommentLedgerEntries(ledgerPath).reverse()) {
10411
+ if (entry?.key !== key) continue;
10412
+ const postedAtMs = new Date(entry.postedAt || entry.recordedAt || entry.at || 0).getTime();
10413
+ if (Number.isFinite(postedAtMs) && Number.isFinite(nowMs) && nowMs - postedAtMs <= windowMs) {
10414
+ return { post: false, key, reason: "recent_duplicate" };
10415
+ }
10416
+ if (entry?.message === message) return { post: false, key, reason: "latest_equivalent" };
10417
+ }
10418
+ return { post: true, key };
10419
+ }
10420
+ function recordClickUpWorktreeFailureComment({ ledgerPath, taskId, message, posted = true, skippedReason = null, at = /* @__PURE__ */ new Date() } = {}) {
10421
+ appendClickUpCommentLedgerEntry(ledgerPath, {
10422
+ key: clickUpWorktreeFailureCommentKey({ taskId, message }),
10423
+ taskId,
10424
+ eventType: "worktree_failure",
10425
+ action: posted ? "worktree_failure_comment_posted" : "worktree_failure_comment_skipped",
10426
+ message,
10427
+ posted,
10428
+ skippedReason,
10429
+ postedAt: at.toISOString()
10430
+ });
10431
+ }
10312
10432
  function clickUpTaskIdFromPayload(payload = {}) {
10313
10433
  return String(payload.task_id || payload.taskId || payload.task?.id || payload.task?.task_id || "").trim();
10314
10434
  }