@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 +1 -1
- package/Agents_Common.prompt.md +1 -1
- package/assets/agents/workflow_product_manager.md +10 -9
- package/dist/index.js +190 -70
- package/dist/sanitize_cli.js +190 -70
- 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
|
|
|
@@ -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
|
|
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
|
|
@@ -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.
|
|
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 `
|
|
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
|
@@ -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",
|
|
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: "
|
|
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(
|
|
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(
|
|
9250
|
+
throw new Error(`${serviceName} ${endpoint} failed: ${response.status} ${message}`);
|
|
9250
9251
|
}
|
|
9251
9252
|
return data;
|
|
9252
9253
|
}
|
|
9253
|
-
async function
|
|
9254
|
-
if (typeof fetchImpl !== "function") throw new Error(
|
|
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({
|
|
9300
|
-
const projects = await
|
|
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({
|
|
9312
|
+
async function refreshOpenChamberProjectCopy({ opencodeBaseUrl, projectId, fetchImpl = globalThis.fetch } = {}) {
|
|
9306
9313
|
if (!projectId) return { refreshed: false, reason: "project_id_unavailable" };
|
|
9307
|
-
|
|
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
|
|
9324
|
-
|
|
9325
|
-
const
|
|
9326
|
-
|
|
9327
|
-
await
|
|
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({
|
|
9330
|
-
|
|
9331
|
-
|
|
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(`
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
9403
|
-
const parentVisibility = await syncOpenChamberWorktreeVisibility({
|
|
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({
|
|
9411
|
-
const visibility = await syncOpenChamberWorktreeVisibility({
|
|
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,
|
|
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
|
-
|
|
9426
|
-
|
|
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
|
|
9429
|
-
|
|
9430
|
-
|
|
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" &&
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 && !
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
10150
|
-
|
|
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
|
|
10231
|
-
|
|
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
|
-
|
|
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
|
}
|