@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/dist/sanitize_cli.js
CHANGED
|
@@ -8534,6 +8534,7 @@ var CLICKUP_WEBHOOK_REQUEST_TIMEOUT_MS = 1e4;
|
|
|
8534
8534
|
var CLICKUP_WEBHOOK_STARTUP_TASK_LIMIT = 50;
|
|
8535
8535
|
var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
|
|
8536
8536
|
var CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS = 3e4;
|
|
8537
|
+
var CLICKUP_WORKTREE_FAILURE_COMMENT_DEDUPE_MS = 10 * 60 * 1e3;
|
|
8537
8538
|
var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
|
|
8538
8539
|
var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
|
|
8539
8540
|
var DISCUSSION_BACKFILL_FETCH_LIMIT = 100;
|
|
@@ -8851,9 +8852,9 @@ var CLICKUP_REQUIRED_SUMMARY_SECTIONS = [
|
|
|
8851
8852
|
];
|
|
8852
8853
|
var CLICKUP_RAW_LOG_SECTION_NAMES = /* @__PURE__ */ new Set(["Raw Logs", "Logs", "Full Logs", "Command Output", "Transcript"]);
|
|
8853
8854
|
var CLICKUP_TRANSITIONS = /* @__PURE__ */ new Map([
|
|
8854
|
-
["plan->in progress", { status: "in progress",
|
|
8855
|
+
["plan->in progress", { status: "in progress", comment: "Plan complete; moving to implementation without generic CTO/PO assignment." }],
|
|
8855
8856
|
["in progress->validation", { status: "validation", comment: "Implementation complete; ready for validation." }],
|
|
8856
|
-
["validation->merge", { status: "merge", assignFinalApprovers: true, comment: "
|
|
8857
|
+
["validation->merge", { status: "merge", assignFinalApprovers: true, parentOnlyFinalApproval: true, comment: "Parent validation passed with a functional preview URL; ready for CTO/PO approval flow." }],
|
|
8857
8858
|
["validation->in progress", { status: "in progress", comment: "Validation failed; returning to implementation." }],
|
|
8858
8859
|
["merge->completed", { status: "completed", comment: "Merge complete; closing delivery task." }],
|
|
8859
8860
|
["completed->in progress", { status: "in progress", comment: "Task reopened; returning to implementation." }],
|
|
@@ -9241,30 +9242,36 @@ function openChamberUrl(baseUrl, pathname, query = {}) {
|
|
|
9241
9242
|
}
|
|
9242
9243
|
return url.toString();
|
|
9243
9244
|
}
|
|
9244
|
-
async function readOpenChamberJson(response, endpoint) {
|
|
9245
|
+
async function readOpenChamberJson(response, endpoint, serviceName = "OpenChamber") {
|
|
9245
9246
|
const text = await response.text();
|
|
9246
9247
|
let data = null;
|
|
9247
9248
|
if (text.trim()) {
|
|
9248
9249
|
try {
|
|
9249
9250
|
data = JSON.parse(text);
|
|
9250
9251
|
} catch {
|
|
9251
|
-
throw new Error(
|
|
9252
|
+
throw new Error(`${serviceName} ${endpoint} returned non-JSON response.`);
|
|
9252
9253
|
}
|
|
9253
9254
|
}
|
|
9254
9255
|
if (!response.ok) {
|
|
9255
9256
|
const message = data?.error?.message || data?.message || data?.data?.message || text.slice(0, 200) || `HTTP ${response.status}`;
|
|
9256
|
-
throw new Error(
|
|
9257
|
+
throw new Error(`${serviceName} ${endpoint} failed: ${response.status} ${message}`);
|
|
9257
9258
|
}
|
|
9258
9259
|
return data;
|
|
9259
9260
|
}
|
|
9260
|
-
async function
|
|
9261
|
-
if (typeof fetchImpl !== "function") throw new Error(
|
|
9261
|
+
async function requestServiceJson({ baseUrl, serviceName = "OpenChamber", endpoint, method = "GET", directory, body, fetchImpl = globalThis.fetch } = {}) {
|
|
9262
|
+
if (typeof fetchImpl !== "function") throw new Error(`${serviceName} API calls require fetch.`);
|
|
9262
9263
|
const response = await fetchImpl(openChamberUrl(baseUrl, endpoint, { directory }), {
|
|
9263
9264
|
method,
|
|
9264
9265
|
headers: body ? { "content-type": "application/json" } : void 0,
|
|
9265
9266
|
body: body ? JSON.stringify(body) : void 0
|
|
9266
9267
|
});
|
|
9267
|
-
return readOpenChamberJson(response, endpoint);
|
|
9268
|
+
return readOpenChamberJson(response, endpoint, serviceName);
|
|
9269
|
+
}
|
|
9270
|
+
async function requestOpenChamberJson(options = {}) {
|
|
9271
|
+
return requestServiceJson({ ...options, serviceName: "OpenChamber" });
|
|
9272
|
+
}
|
|
9273
|
+
async function requestOpenCodeJson(options = {}) {
|
|
9274
|
+
return requestServiceJson({ ...options, serviceName: "OpenCode" });
|
|
9268
9275
|
}
|
|
9269
9276
|
function normalizeOpenChamberCollection(value) {
|
|
9270
9277
|
if (Array.isArray(value)) return value;
|
|
@@ -9303,39 +9310,40 @@ function openChamberListIncludesBranch(list, directory, branch) {
|
|
|
9303
9310
|
return !entryBranch || entryBranch === branch;
|
|
9304
9311
|
});
|
|
9305
9312
|
}
|
|
9306
|
-
async function findOpenChamberProject({
|
|
9307
|
-
const projects = await
|
|
9313
|
+
async function findOpenChamberProject({ opencodeBaseUrl, baseWorktree, fetchImpl = globalThis.fetch } = {}) {
|
|
9314
|
+
const projects = await requestOpenCodeJson({ baseUrl: opencodeBaseUrl, endpoint: "/project", directory: baseWorktree, fetchImpl });
|
|
9308
9315
|
if (!Array.isArray(projects)) return null;
|
|
9309
9316
|
const resolvedBase = path5.resolve(baseWorktree);
|
|
9310
9317
|
return projects.find((project) => path5.resolve(String(project?.worktree || project?.path || "")) === resolvedBase) || null;
|
|
9311
9318
|
}
|
|
9312
|
-
async function refreshOpenChamberProjectCopy({
|
|
9319
|
+
async function refreshOpenChamberProjectCopy({ opencodeBaseUrl, projectId, fetchImpl = globalThis.fetch } = {}) {
|
|
9313
9320
|
if (!projectId) return { refreshed: false, reason: "project_id_unavailable" };
|
|
9314
|
-
|
|
9315
|
-
await readOpenChamberJson(response, "/experimental/project/{projectID}/copy/refresh");
|
|
9321
|
+
await requestOpenCodeJson({ baseUrl: opencodeBaseUrl, endpoint: `/experimental/project/${encodeURIComponent(projectId)}/copy/refresh`, method: "POST", fetchImpl });
|
|
9316
9322
|
return { refreshed: true, projectId };
|
|
9317
9323
|
}
|
|
9318
|
-
async function listOpenChamberGitWorktrees({ baseUrl, baseWorktree, fetchImpl = globalThis.fetch } = {}) {
|
|
9319
|
-
return requestOpenChamberJson({ baseUrl, endpoint: "/api/git/worktrees", directory: baseWorktree, fetchImpl });
|
|
9324
|
+
async function listOpenChamberGitWorktrees({ openchamberBaseUrl, baseUrl, baseWorktree, fetchImpl = globalThis.fetch } = {}) {
|
|
9325
|
+
return requestOpenChamberJson({ baseUrl: openchamberBaseUrl || baseUrl, endpoint: "/api/git/worktrees", directory: baseWorktree, fetchImpl });
|
|
9320
9326
|
}
|
|
9321
|
-
async function verifyOpenChamberGitWorktree({ baseUrl, baseWorktree, worktreePath, branch, fetchImpl = globalThis.fetch } = {}) {
|
|
9322
|
-
const worktrees = await listOpenChamberGitWorktrees({ baseUrl, baseWorktree, fetchImpl });
|
|
9327
|
+
async function verifyOpenChamberGitWorktree({ openchamberBaseUrl, baseUrl, baseWorktree, worktreePath, branch, fetchImpl = globalThis.fetch } = {}) {
|
|
9328
|
+
const worktrees = await listOpenChamberGitWorktrees({ openchamberBaseUrl: openchamberBaseUrl || baseUrl, baseWorktree, fetchImpl });
|
|
9323
9329
|
const verified = openChamberListIncludesBranch(worktrees, worktreePath, branch);
|
|
9324
9330
|
if (!verified) {
|
|
9325
9331
|
throw new Error(`OpenChamber Git worktree verification failed for ${worktreePath} on ${branch}.`);
|
|
9326
9332
|
}
|
|
9327
9333
|
return { worktree: true, branch: true };
|
|
9328
9334
|
}
|
|
9329
|
-
async function syncOpenChamberWorktreeVisibility({ baseUrl, baseWorktree, worktreePath, branch, fetchImpl = globalThis.fetch } = {}) {
|
|
9330
|
-
const
|
|
9331
|
-
|
|
9332
|
-
const
|
|
9333
|
-
|
|
9334
|
-
await
|
|
9335
|
+
async function syncOpenChamberWorktreeVisibility({ openchamberBaseUrl, opencodeBaseUrl, baseUrl, baseWorktree, worktreePath, branch, fetchImpl = globalThis.fetch } = {}) {
|
|
9336
|
+
const effectiveOpenChamberBaseUrl = openchamberBaseUrl || baseUrl;
|
|
9337
|
+
const effectiveOpenCodeBaseUrl = opencodeBaseUrl || baseUrl;
|
|
9338
|
+
const gitWorktree = await verifyOpenChamberGitWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, worktreePath, branch, fetchImpl });
|
|
9339
|
+
await requestOpenCodeJson({ baseUrl: effectiveOpenCodeBaseUrl, endpoint: "/experimental/workspace/sync-list", method: "POST", directory: baseWorktree, fetchImpl });
|
|
9340
|
+
const project = await findOpenChamberProject({ opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, fetchImpl });
|
|
9341
|
+
if (!project?.id) throw new Error("OpenCode project was not found after workspace sync; refusing to treat worktree as visible.");
|
|
9342
|
+
await refreshOpenChamberProjectCopy({ opencodeBaseUrl: effectiveOpenCodeBaseUrl, projectId: project.id, fetchImpl });
|
|
9335
9343
|
const [worktrees, workspaces, directories] = await Promise.all([
|
|
9336
|
-
listOpenChamberGitWorktrees({
|
|
9337
|
-
|
|
9338
|
-
|
|
9344
|
+
listOpenChamberGitWorktrees({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, fetchImpl }),
|
|
9345
|
+
requestOpenCodeJson({ baseUrl: effectiveOpenCodeBaseUrl, endpoint: "/experimental/workspace", directory: baseWorktree, fetchImpl }),
|
|
9346
|
+
requestOpenCodeJson({ baseUrl: effectiveOpenCodeBaseUrl, endpoint: `/project/${encodeURIComponent(project.id)}/directories`, directory: baseWorktree, fetchImpl })
|
|
9339
9347
|
]);
|
|
9340
9348
|
const visibility = {
|
|
9341
9349
|
worktree: openChamberListIncludesBranch(worktrees, worktreePath, branch),
|
|
@@ -9345,7 +9353,7 @@ async function syncOpenChamberWorktreeVisibility({ baseUrl, baseWorktree, worktr
|
|
|
9345
9353
|
projectId: project.id
|
|
9346
9354
|
};
|
|
9347
9355
|
if (!visibility.worktree || !visibility.workspace || !visibility.projectDirectory) {
|
|
9348
|
-
throw new Error(`
|
|
9356
|
+
throw new Error(`OpenCode visibility verification failed for ${worktreePath}: ${JSON.stringify(visibility)}`);
|
|
9349
9357
|
}
|
|
9350
9358
|
return visibility;
|
|
9351
9359
|
}
|
|
@@ -9354,12 +9362,13 @@ function assertOpenChamberClickUpWorktreePath({ baseWorktree, worktreePath } = {
|
|
|
9354
9362
|
throw new Error(`OpenChamber worktree path is outside the configured ClickUp sibling scope: ${worktreePath}`);
|
|
9355
9363
|
}
|
|
9356
9364
|
}
|
|
9357
|
-
async function createOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch, worktreePath, startPoint, branchExists = false, fetchImpl = globalThis.fetch } = {}) {
|
|
9365
|
+
async function createOpenChamberClickUpWorktree({ openchamberBaseUrl, baseUrl, baseWorktree, branch, worktreePath, startPoint, branchExists = false, fetchImpl = globalThis.fetch } = {}) {
|
|
9366
|
+
const effectiveOpenChamberBaseUrl = openchamberBaseUrl || baseUrl;
|
|
9358
9367
|
const worktreeName = clickUpOpenChamberWorktreeName(branch);
|
|
9359
9368
|
const body = { name: worktreeName, mode: branchExists ? "existing" : "new", worktreeName, branchName: branch, startRef: branchExists ? branch : startPoint };
|
|
9360
9369
|
let created;
|
|
9361
9370
|
try {
|
|
9362
|
-
created = await requestOpenChamberJson({ baseUrl, endpoint: "/api/git/worktrees", method: "POST", directory: baseWorktree, body, fetchImpl });
|
|
9371
|
+
created = await requestOpenChamberJson({ baseUrl: effectiveOpenChamberBaseUrl, endpoint: "/api/git/worktrees", method: "POST", directory: baseWorktree, body, fetchImpl });
|
|
9363
9372
|
} catch (error) {
|
|
9364
9373
|
throw new Error(`OpenChamber could not create ${branch} from ${branchExists ? branch : startPoint}; API may not support required branch/startRef semantics. Fail-closed: ${error.message}`);
|
|
9365
9374
|
}
|
|
@@ -9371,15 +9380,17 @@ async function createOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch,
|
|
|
9371
9380
|
if (createdBranch !== branch) {
|
|
9372
9381
|
throw new Error(`OpenChamber created unexpected branch ${createdBranch || "<unknown>"}; expected ${branch}.`);
|
|
9373
9382
|
}
|
|
9374
|
-
const verified = await verifyOpenChamberGitWorktree({
|
|
9383
|
+
const verified = await verifyOpenChamberGitWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, worktreePath: createdDirectory, branch, fetchImpl });
|
|
9375
9384
|
return { created, worktree: path5.resolve(createdDirectory), branch: createdBranch, verified };
|
|
9376
9385
|
}
|
|
9377
|
-
async function registerOpenChamberClickUpWorktree({ baseUrl, baseWorktree, branch, worktreePath, fetchImpl = globalThis.fetch, source = "reuse" } = {}) {
|
|
9386
|
+
async function registerOpenChamberClickUpWorktree({ openchamberBaseUrl, opencodeBaseUrl, baseUrl, baseWorktree, branch, worktreePath, fetchImpl = globalThis.fetch, source = "reuse" } = {}) {
|
|
9378
9387
|
assertOpenChamberClickUpWorktreePath({ baseWorktree, worktreePath });
|
|
9379
|
-
const visibility = await syncOpenChamberWorktreeVisibility({ baseUrl, baseWorktree, worktreePath, branch, fetchImpl });
|
|
9388
|
+
const visibility = await syncOpenChamberWorktreeVisibility({ openchamberBaseUrl: openchamberBaseUrl || baseUrl, opencodeBaseUrl: opencodeBaseUrl || baseUrl, baseWorktree, worktreePath, branch, fetchImpl });
|
|
9380
9389
|
return { branch, worktree: path5.resolve(worktreePath), reused: true, provider: "openchamber", openChamber: { source, visibility } };
|
|
9381
9390
|
}
|
|
9382
|
-
async function ensureClickUpTaskWorktreeOpenChamber({ baseWorktree = "", taskId, taskType = "Tarea", parentTaskId = "", subtaskId = "", existingMetadata = {}, runGitFn = runGit, baseUrl = "", fetchImpl = globalThis.fetch, log = null } = {}) {
|
|
9391
|
+
async function ensureClickUpTaskWorktreeOpenChamber({ baseWorktree = "", taskId, taskType = "Tarea", parentTaskId = "", subtaskId = "", existingMetadata = {}, runGitFn = runGit, openchamberBaseUrl = "", opencodeBaseUrl = "", baseUrl = "", fetchImpl = globalThis.fetch, log = null } = {}) {
|
|
9392
|
+
const effectiveOpenChamberBaseUrl = openchamberBaseUrl || baseUrl;
|
|
9393
|
+
const effectiveOpenCodeBaseUrl = opencodeBaseUrl || baseUrl;
|
|
9383
9394
|
const effectiveParent = parentTaskId || taskId;
|
|
9384
9395
|
const isSubtask = isClickUpSubtaskRoute({ taskType, parentTaskId: effectiveParent, subtaskId, taskId });
|
|
9385
9396
|
const parentBranch = isSubtask ? deriveClickUpBranchName({ taskType, parentTaskId: effectiveParent }) : "";
|
|
@@ -9387,13 +9398,13 @@ async function ensureClickUpTaskWorktreeOpenChamber({ baseWorktree = "", taskId,
|
|
|
9387
9398
|
const prTarget = parentBranch || "dev";
|
|
9388
9399
|
const existing = safeExistingClickUpWorktree({ metadata: existingMetadata, branch });
|
|
9389
9400
|
if (existing) {
|
|
9390
|
-
const registered = await registerOpenChamberClickUpWorktree({
|
|
9401
|
+
const registered = await registerOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, branch, worktreePath: existing.worktree, fetchImpl, source: "metadata_reuse" });
|
|
9391
9402
|
log?.({ type: "openchamber_worktree_registered", taskId, branch, worktree: registered.worktree, source: "metadata_reuse", visibility: registered.openChamber.visibility });
|
|
9392
9403
|
return { ...registered, parentBranch: parentBranch || void 0, prTarget };
|
|
9393
9404
|
}
|
|
9394
9405
|
const worktreePath = deriveClickUpWorktree({ baseWorktree, taskId, taskType, parentTaskId: effectiveParent, subtaskId });
|
|
9395
9406
|
if (fs5.existsSync(worktreePath)) {
|
|
9396
|
-
const registered = await registerOpenChamberClickUpWorktree({
|
|
9407
|
+
const registered = await registerOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, branch, worktreePath, fetchImpl, source: "existing_directory" });
|
|
9397
9408
|
log?.({ type: "openchamber_worktree_registered", taskId, branch, worktree: registered.worktree, source: "existing_directory", visibility: registered.openChamber.visibility });
|
|
9398
9409
|
return { ...registered, parentBranch: parentBranch || void 0, prTarget };
|
|
9399
9410
|
}
|
|
@@ -9401,40 +9412,54 @@ async function ensureClickUpTaskWorktreeOpenChamber({ baseWorktree = "", taskId,
|
|
|
9401
9412
|
if (isSubtask) {
|
|
9402
9413
|
const parentWorktree = deriveClickUpWorktree({ baseWorktree, taskId: effectiveParent, taskType, parentTaskId: effectiveParent });
|
|
9403
9414
|
if (fs5.existsSync(parentWorktree)) {
|
|
9404
|
-
const registeredParent = await registerOpenChamberClickUpWorktree({
|
|
9415
|
+
const registeredParent = await registerOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, branch: parentBranch, worktreePath: parentWorktree, fetchImpl, source: "parent_existing_directory" });
|
|
9405
9416
|
parentBootstrap = { branch: parentBranch, worktree: registeredParent.worktree, reused: true, provider: "openchamber", visibility: registeredParent.openChamber.visibility };
|
|
9406
9417
|
} else {
|
|
9407
9418
|
const parentStartPoint = resolveClickUpDevStartPoint(baseWorktree, runGitFn);
|
|
9408
9419
|
const parentBranchExists = clickUpGitRefExists(baseWorktree, parentBranch, runGitFn);
|
|
9409
|
-
const createdParent = await createOpenChamberClickUpWorktree({
|
|
9410
|
-
const parentVisibility = await syncOpenChamberWorktreeVisibility({
|
|
9420
|
+
const createdParent = await createOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, branch: parentBranch, worktreePath: parentWorktree, startPoint: parentStartPoint, branchExists: parentBranchExists, fetchImpl });
|
|
9421
|
+
const parentVisibility = await syncOpenChamberWorktreeVisibility({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, worktreePath: createdParent.worktree, branch: parentBranch, fetchImpl });
|
|
9411
9422
|
parentBootstrap = { branch: parentBranch, worktree: createdParent.worktree, startPoint: parentBranchExists ? parentBranch : parentStartPoint, reused: parentBranchExists, provider: "openchamber", visibility: parentVisibility };
|
|
9412
9423
|
log?.({ type: "openchamber_worktree_created", taskId: effectiveParent, branch: parentBranch, worktree: parentBootstrap.worktree, startPoint: parentBootstrap.startPoint, visibility: parentVisibility });
|
|
9413
9424
|
}
|
|
9414
9425
|
}
|
|
9415
9426
|
const startPoint = isSubtask ? parentBranch : resolveClickUpDevStartPoint(baseWorktree, runGitFn);
|
|
9416
9427
|
const branchExists = clickUpGitRefExists(baseWorktree, branch, runGitFn);
|
|
9417
|
-
const created = await createOpenChamberClickUpWorktree({
|
|
9418
|
-
const visibility = await syncOpenChamberWorktreeVisibility({
|
|
9428
|
+
const created = await createOpenChamberClickUpWorktree({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, baseWorktree, branch, worktreePath, startPoint, branchExists, fetchImpl });
|
|
9429
|
+
const visibility = await syncOpenChamberWorktreeVisibility({ openchamberBaseUrl: effectiveOpenChamberBaseUrl, opencodeBaseUrl: effectiveOpenCodeBaseUrl, baseWorktree, worktreePath: created.worktree, branch, fetchImpl });
|
|
9419
9430
|
log?.({ type: "openchamber_worktree_created", taskId, branch, worktree: created.worktree, startPoint: branchExists ? branch : startPoint, visibility });
|
|
9420
9431
|
return { branch, worktree: created.worktree, reused: false, startPoint, parentBranch: parentBranch || void 0, prTarget, parentBootstrap: parentBootstrap || void 0, provider: "openchamber", openChamber: { source: "created", visibility } };
|
|
9421
9432
|
}
|
|
9422
9433
|
async function ensureClickUpTaskWorktreeForWebhook({ opencodeBaseUrl = "", opencodeBaseUrlConfigured = false, openchamberBaseUrl = "", openchamberBaseUrlConfigured = false, clickupClient = null, webhookWorktree = process.cwd(), fetchImpl = globalThis.fetch, ...options } = {}) {
|
|
9423
|
-
void opencodeBaseUrl;
|
|
9424
9434
|
void opencodeBaseUrlConfigured;
|
|
9425
9435
|
if (!openchamberBaseUrlConfigured || !openchamberBaseUrl) return ensureClickUpTaskWorktree(options);
|
|
9426
9436
|
try {
|
|
9427
|
-
return await ensureClickUpTaskWorktreeOpenChamber({ ...options,
|
|
9437
|
+
return await ensureClickUpTaskWorktreeOpenChamber({ ...options, openchamberBaseUrl, opencodeBaseUrl, fetchImpl, log: (entry) => appendClickUpWebhookLocalLog(webhookWorktree, entry) });
|
|
9428
9438
|
} catch (error) {
|
|
9429
9439
|
const message = `OpenChamber worktree provisioning failed for ${options.taskId || "unknown task"}; raw git fallback is disabled to avoid invisible worktrees. ${error.message}`;
|
|
9430
9440
|
appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_failed", taskId: options.taskId || null, message });
|
|
9431
9441
|
if (typeof clickupClient?.postTaskComment === "function") {
|
|
9432
|
-
|
|
9433
|
-
|
|
9442
|
+
const taskId = options.taskId || "unknown task";
|
|
9443
|
+
const ledgerPath = clickUpCommentLedgerPath(webhookWorktree);
|
|
9444
|
+
const comment = `${message}
|
|
9434
9445
|
|
|
9435
|
-
Optima did not run raw git worktree fallback. Please verify clickup.openchamber.base_url and OpenChamber
|
|
9436
|
-
|
|
9437
|
-
|
|
9446
|
+
Optima did not run raw git worktree fallback. Please verify clickup.openchamber.base_url and OpenCode/OpenChamber endpoint configuration.`;
|
|
9447
|
+
let dedupe = { post: true, key: clickUpWorktreeFailureCommentKey({ taskId, message }) };
|
|
9448
|
+
try {
|
|
9449
|
+
dedupe = shouldPostClickUpWorktreeFailureComment({ ledgerPath, taskId, message });
|
|
9450
|
+
} catch (ledgerError) {
|
|
9451
|
+
appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_comment_dedupe_failed", taskId, message: ledgerError.message });
|
|
9452
|
+
}
|
|
9453
|
+
if (dedupe.post) {
|
|
9454
|
+
try {
|
|
9455
|
+
await clickupClient.postTaskComment({ taskId: options.taskId, comment });
|
|
9456
|
+
recordClickUpWorktreeFailureComment({ ledgerPath, taskId, message, posted: true });
|
|
9457
|
+
} catch (commentError) {
|
|
9458
|
+
appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_blocker_comment_failed", taskId: options.taskId || null, message: commentError.message });
|
|
9459
|
+
}
|
|
9460
|
+
} else {
|
|
9461
|
+
appendClickUpWebhookLocalLog(webhookWorktree, { type: "openchamber_worktree_blocker_comment_deduped", taskId, key: dedupe.key, reason: dedupe.reason });
|
|
9462
|
+
recordClickUpWorktreeFailureComment({ ledgerPath, taskId, message, posted: false, skippedReason: dedupe.reason });
|
|
9438
9463
|
}
|
|
9439
9464
|
}
|
|
9440
9465
|
throw new Error(message);
|
|
@@ -9568,11 +9593,14 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
|
|
|
9568
9593
|
const key = `${from}->${to}`;
|
|
9569
9594
|
const rule = CLICKUP_TRANSITIONS.get(key);
|
|
9570
9595
|
if (!rule) return { ok: false, dryRun: true, message: `Transition not allowed: ${key}` };
|
|
9571
|
-
const assignsFinalApprovers = rule.assignFinalApprovers === true;
|
|
9572
|
-
const requiresPlanCompletionContract = from === "plan" &&
|
|
9596
|
+
const assignsFinalApprovers = rule.assignFinalApprovers === true && !(rule.parentOnlyFinalApproval && isSubtask);
|
|
9597
|
+
const requiresPlanCompletionContract = from === "plan" && to === "in progress";
|
|
9573
9598
|
const definitionContent = compactMarkdownValue(definition);
|
|
9574
9599
|
const description = compactMarkdownValue(planDescription);
|
|
9575
9600
|
const validationErrors = [];
|
|
9601
|
+
if (rule.parentOnlyFinalApproval && isSubtask) {
|
|
9602
|
+
validationErrors.push("Subtasks must not use the parent final-approval transition; merge validated subtasks directly into the parent branch/workspace.");
|
|
9603
|
+
}
|
|
9576
9604
|
if (assignsFinalApprovers && !isRealClickUpAssigneeId(productManagerAssignee) && requireProductManagerAssignee !== false) {
|
|
9577
9605
|
validationErrors.push("productManagerAssignee must be the configured ClickUp PM assignee ID before assigning final approvers.");
|
|
9578
9606
|
}
|
|
@@ -9581,10 +9609,10 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
|
|
|
9581
9609
|
if (!definitionContent) validationErrors.push("definition is required and must contain the complete plan/Definition content at plan completion.");
|
|
9582
9610
|
}
|
|
9583
9611
|
if (validationErrors.length > 0) return clickUpPayloadValidationError(validationErrors);
|
|
9584
|
-
const finalApprovers =
|
|
9612
|
+
const finalApprovers = assignsFinalApprovers ? finalApprovalAssignees(CLICKUP_FINAL_APPROVER_ROLES, humansRegistry) : [];
|
|
9585
9613
|
const explicitRemovals = (removeAssignees || []).filter(Boolean);
|
|
9586
9614
|
const normalizedProductManagerAssignee = isRealClickUpAssigneeId(productManagerAssignee) ? String(productManagerAssignee).trim() : "";
|
|
9587
|
-
const removalTargets =
|
|
9615
|
+
const removalTargets = assignsFinalApprovers ? [...new Set([normalizedProductManagerAssignee, ...explicitRemovals].filter(Boolean))] : explicitRemovals;
|
|
9588
9616
|
const assigned = [...new Set([...assignees || [], ...finalApprovers].filter(Boolean))].filter((assignee) => !removalTargets.includes(assignee));
|
|
9589
9617
|
const authority = from === "validation" || to === "merge" ? determineClickUpMergeAuthority({ isSubtask, clickupStatus: to, validationPassed: validationPassed || to === "merge", humansRegistry }) : null;
|
|
9590
9618
|
const fields = authority ? { merge_authority: JSON.stringify(authority) } : {};
|
|
@@ -9599,7 +9627,7 @@ function buildClickUpTransitionPayload({ fromStatus, toStatus, validationPassed
|
|
|
9599
9627
|
assignment_delta: {
|
|
9600
9628
|
add: assigned,
|
|
9601
9629
|
remove: removalTargets,
|
|
9602
|
-
objective:
|
|
9630
|
+
objective: assignsFinalApprovers ? "zero_product_manager_assigned_tasks" : "preserve_existing_owner_policy"
|
|
9603
9631
|
},
|
|
9604
9632
|
comment: rule.comment,
|
|
9605
9633
|
description,
|
|
@@ -10057,11 +10085,17 @@ function createTestClickUpApiClient(config) {
|
|
|
10057
10085
|
__metadata: metadata
|
|
10058
10086
|
};
|
|
10059
10087
|
}
|
|
10060
|
-
function
|
|
10061
|
-
const webhook = response?.webhook || response?.data || response || {};
|
|
10088
|
+
function clickUpWebhookRemoteInactiveReason(webhook = {}) {
|
|
10062
10089
|
const healthStatus = String(webhook.health?.status || webhook.health_status || "").trim().toLowerCase();
|
|
10063
10090
|
const status = String(webhook.status || webhook.state || "").trim().toLowerCase();
|
|
10064
|
-
|
|
10091
|
+
if (["suspended", "paused", "failed", "failing", "unhealthy", "error", "errored"].includes(healthStatus)) return `remote_health_${healthStatus}`;
|
|
10092
|
+
if (["inactive", "disabled", "suspended", "failed", "failing", "error", "errored", "paused"].includes(status)) return `remote_status_${status}`;
|
|
10093
|
+
if (webhook.active === false) return "remote_inactive";
|
|
10094
|
+
return "";
|
|
10095
|
+
}
|
|
10096
|
+
function normalizeClickUpWebhookApiResponse(response = {}, fallbackConfig = null) {
|
|
10097
|
+
const webhook = response?.webhook || response?.data || response || {};
|
|
10098
|
+
const active = !clickUpWebhookRemoteInactiveReason(webhook);
|
|
10065
10099
|
return sanitizeClickUpWebhookState({
|
|
10066
10100
|
active,
|
|
10067
10101
|
webhookId: webhook.id || webhook.webhook_id || webhook.webhookId,
|
|
@@ -10087,20 +10121,44 @@ function clickUpWebhookLocationCompatible(webhook = {}, config = {}) {
|
|
|
10087
10121
|
}
|
|
10088
10122
|
return true;
|
|
10089
10123
|
}
|
|
10124
|
+
function clickUpWebhookEventsCompatible(webhook = {}, config = {}) {
|
|
10125
|
+
if (!Array.isArray(webhook.events) || webhook.events.length === 0) return false;
|
|
10126
|
+
const actualEvents = new Set(webhook.events);
|
|
10127
|
+
return [...new Set(config?.webhook?.events || [])].every((event) => actualEvents.has(event));
|
|
10128
|
+
}
|
|
10129
|
+
function clickUpWebhookRemoteCompatibilityReason(webhook = {}, config = {}) {
|
|
10130
|
+
if (!clickUpWebhookLocationCompatible(webhook, config)) return "remote_location_mismatch";
|
|
10131
|
+
if (!clickUpWebhookEventsCompatible(webhook, config)) return "remote_events_mismatch";
|
|
10132
|
+
return "";
|
|
10133
|
+
}
|
|
10134
|
+
function isClickUpWebhookRemoteSelfHealableReason(reason = "") {
|
|
10135
|
+
const normalized = String(reason || "");
|
|
10136
|
+
return normalized === "remote_inactive" || normalized.startsWith("remote_health_") || normalized.startsWith("remote_status_");
|
|
10137
|
+
}
|
|
10090
10138
|
async function findReusableClickUpWebhook(config, clickupClient = null, existingState = null) {
|
|
10091
10139
|
if (!clickupClient?.listWebhooks) return null;
|
|
10092
10140
|
const listed = await clickupClient.listWebhooks({ teamId: config.teamId });
|
|
10093
|
-
let
|
|
10141
|
+
let incompatibleMatch = null;
|
|
10094
10142
|
for (const webhook of clickUpWebhookListItems(listed)) {
|
|
10095
10143
|
const remote = normalizeClickUpWebhookApiResponse(webhook, config);
|
|
10096
10144
|
if (remote.publicUrl !== config.webhook.publicUrl) continue;
|
|
10097
|
-
|
|
10145
|
+
const compatibilityReason = clickUpWebhookRemoteCompatibilityReason(webhook, config);
|
|
10146
|
+
if (compatibilityReason) {
|
|
10147
|
+
if (compatibilityReason === "remote_events_mismatch" && remote.webhookId && !incompatibleMatch) incompatibleMatch = { ...remote, active: false, reason: compatibilityReason };
|
|
10148
|
+
continue;
|
|
10149
|
+
}
|
|
10098
10150
|
const existingSecret = existingState?.webhookId === remote.webhookId ? existingState.secret : "";
|
|
10099
10151
|
const reusable = remote.secret ? remote : { ...remote, secret: existingSecret };
|
|
10100
10152
|
if (isClickUpWebhookStateActive(reusable, config)) return reusable;
|
|
10101
|
-
if (remote.webhookId && !
|
|
10153
|
+
if (remote.webhookId && !incompatibleMatch) {
|
|
10154
|
+
incompatibleMatch = {
|
|
10155
|
+
...remote,
|
|
10156
|
+
active: false,
|
|
10157
|
+
reason: remote.active === false ? clickUpWebhookRemoteInactiveReason(webhook) || "remote_inactive" : "remote_secret_unavailable"
|
|
10158
|
+
};
|
|
10159
|
+
}
|
|
10102
10160
|
}
|
|
10103
|
-
return
|
|
10161
|
+
return incompatibleMatch;
|
|
10104
10162
|
}
|
|
10105
10163
|
async function validateClickUpWebhookState(state, config, clickupClient = null, { allowRemoteUnhealthyLocalRecovery = false } = {}) {
|
|
10106
10164
|
if (!isClickUpWebhookStateActive(state, config)) return { valid: false, reason: "state_incomplete" };
|
|
@@ -10115,7 +10173,7 @@ async function validateClickUpWebhookState(state, config, clickupClient = null,
|
|
|
10115
10173
|
}
|
|
10116
10174
|
const localStateValidation = await validateClickUpWebhookState(state, config, null);
|
|
10117
10175
|
if (allowRemoteUnhealthyLocalRecovery && localStateValidation.valid && remote.webhookId === state.webhookId) {
|
|
10118
|
-
const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl &&
|
|
10176
|
+
const remoteConfigMatches = remote.publicUrl === config.webhook.publicUrl && !clickUpWebhookRemoteCompatibilityReason(match, config);
|
|
10119
10177
|
if (remoteConfigMatches) {
|
|
10120
10178
|
return {
|
|
10121
10179
|
...localStateValidation,
|
|
@@ -10140,12 +10198,18 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
|
|
|
10140
10198
|
const { config } = validation;
|
|
10141
10199
|
const existing = existingState ? sanitizeClickUpWebhookState(existingState, config) : readClickUpWebhookState(worktree, config);
|
|
10142
10200
|
const existingValidation = await validateClickUpWebhookState(existing, config, clickupClient, { allowRemoteUnhealthyLocalRecovery: true });
|
|
10143
|
-
|
|
10201
|
+
const canReplaceUnhealthyExisting = existingValidation.mode === "local_state_remote_unhealthy" && existingValidation.remote?.webhookId && clickupClient?.deleteWebhook && clickupClient?.createWebhook;
|
|
10202
|
+
if (existingValidation.valid && !canReplaceUnhealthyExisting) {
|
|
10144
10203
|
const state = writeClickUpWebhookState(worktree, { ...existingValidation.state, recentEventKeys: existing.recentEventKeys || [] }, config);
|
|
10145
10204
|
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: existingValidation.mode });
|
|
10146
10205
|
return { active: true, valid: true, mode: existingValidation.mode, limitation: existingValidation.limitation, state };
|
|
10147
10206
|
}
|
|
10148
|
-
const reusableRemote =
|
|
10207
|
+
const reusableRemote = canReplaceUnhealthyExisting ? {
|
|
10208
|
+
...existingValidation.remote,
|
|
10209
|
+
active: false,
|
|
10210
|
+
secret: existingValidation.remote.secret || existing.secret,
|
|
10211
|
+
reason: clickUpWebhookRemoteInactiveReason(existingValidation.remote) || existingValidation.reason || "remote_unhealthy_self_heal"
|
|
10212
|
+
} : await findReusableClickUpWebhook(config, clickupClient, existing);
|
|
10149
10213
|
if (reusableRemote) {
|
|
10150
10214
|
if (isClickUpWebhookStateActive(reusableRemote, config)) {
|
|
10151
10215
|
const state = writeClickUpWebhookState(worktree, { ...reusableRemote, recentEventKeys: existing.recentEventKeys || [] }, config);
|
|
@@ -10153,10 +10217,31 @@ async function ensureClickUpWebhookSubscription({ validation, worktree, clickupC
|
|
|
10153
10217
|
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_reused", webhookId: state.webhookId, mode: mode2 });
|
|
10154
10218
|
return { active: true, valid: true, mode: mode2, state };
|
|
10155
10219
|
}
|
|
10156
|
-
|
|
10157
|
-
|
|
10220
|
+
if (reusableRemote.active === false && reusableRemote.reason !== "remote_secret_unavailable") {
|
|
10221
|
+
if (!isClickUpWebhookRemoteSelfHealableReason(reusableRemote.reason)) {
|
|
10222
|
+
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_incompatible" });
|
|
10223
|
+
return { active: false, valid: false, reason: reusableRemote.reason || "remote_incompatible", state: existing, remote: reusableRemote };
|
|
10224
|
+
}
|
|
10225
|
+
if (clickupClient?.deleteWebhook && clickupClient?.createWebhook) {
|
|
10226
|
+
const deleteResult = await deleteClickUpWebhookBestEffort({ webhookId: reusableRemote.webhookId, clickupClient, worktree, reason: reusableRemote.reason || "remote_unhealthy_self_heal" });
|
|
10227
|
+
if (deleteResult.ok) {
|
|
10228
|
+
if (existing.webhookId === reusableRemote.webhookId) markClickUpWebhookInactive(worktree, existing, config);
|
|
10229
|
+
} else {
|
|
10230
|
+
const reason = deleteResult.reason || "remote_unhealthy_delete_failed";
|
|
10231
|
+
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason });
|
|
10232
|
+
return { active: false, valid: false, reason, state: existing, remote: reusableRemote, deleteResult };
|
|
10233
|
+
}
|
|
10234
|
+
} else {
|
|
10235
|
+
const reason = clickupClient?.deleteWebhook ? "remote_unhealthy_create_unavailable" : "remote_unhealthy_delete_unavailable";
|
|
10236
|
+
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason });
|
|
10237
|
+
return { active: false, valid: false, reason, state: existing, remote: reusableRemote };
|
|
10238
|
+
}
|
|
10239
|
+
} else {
|
|
10240
|
+
clickUpWebhookLifecycleLog(worktree, { type: "remote_webhook_preserved", webhookId: reusableRemote.webhookId, reason: reusableRemote.reason || "remote_webhook_exists_without_secret" });
|
|
10241
|
+
return { active: false, valid: false, reason: reusableRemote.reason || "remote_webhook_exists_without_secret", state: existing, remote: reusableRemote };
|
|
10242
|
+
}
|
|
10158
10243
|
}
|
|
10159
|
-
if (existing.webhookId && clickupClient?.deleteWebhook) {
|
|
10244
|
+
if (existing.webhookId && existing.webhookId !== reusableRemote?.webhookId && clickupClient?.deleteWebhook) {
|
|
10160
10245
|
await deleteClickUpWebhookBestEffort({ webhookId: existing.webhookId, clickupClient, worktree, reason: existingValidation.reason || "startup_self_heal" });
|
|
10161
10246
|
markClickUpWebhookInactive(worktree, existing, config);
|
|
10162
10247
|
}
|
|
@@ -10234,23 +10319,29 @@ function rememberClickUpWebhookEvent(state = {}, eventKey, limit = 200) {
|
|
|
10234
10319
|
if (recent.includes(eventKey)) return { duplicate: true, state };
|
|
10235
10320
|
return { duplicate: false, state: { ...state, recentEventKeys: [...recent, eventKey].slice(-limit) } };
|
|
10236
10321
|
}
|
|
10237
|
-
function
|
|
10238
|
-
|
|
10239
|
-
if (!ledgerPath || !fs5.existsSync(ledgerPath)) return processed;
|
|
10322
|
+
function readClickUpCommentLedgerEntries(ledgerPath) {
|
|
10323
|
+
if (!ledgerPath || !fs5.existsSync(ledgerPath)) return [];
|
|
10240
10324
|
try {
|
|
10241
10325
|
const raw = fs5.readFileSync(ledgerPath, "utf8");
|
|
10326
|
+
const entries = [];
|
|
10242
10327
|
for (const [index, line] of raw.split(/\r?\n/).entries()) {
|
|
10243
10328
|
if (!line.trim()) continue;
|
|
10244
10329
|
try {
|
|
10245
|
-
|
|
10246
|
-
if (entry?.key) processed.add(String(entry.key));
|
|
10330
|
+
entries.push(JSON.parse(line));
|
|
10247
10331
|
} catch (error) {
|
|
10248
10332
|
throw new Error(`malformed ledger row ${index + 1}: ${error.message}`);
|
|
10249
10333
|
}
|
|
10250
10334
|
}
|
|
10335
|
+
return entries;
|
|
10251
10336
|
} catch (error) {
|
|
10252
10337
|
throw new Error(`ClickUp comment ledger unavailable: ${error.message}`);
|
|
10253
10338
|
}
|
|
10339
|
+
}
|
|
10340
|
+
function readClickUpCommentLedger(ledgerPath) {
|
|
10341
|
+
const processed = /* @__PURE__ */ new Set();
|
|
10342
|
+
for (const entry of readClickUpCommentLedgerEntries(ledgerPath)) {
|
|
10343
|
+
if (entry?.key) processed.add(String(entry.key));
|
|
10344
|
+
}
|
|
10254
10345
|
return processed;
|
|
10255
10346
|
}
|
|
10256
10347
|
function appendClickUpCommentLedgerEntry(ledgerPath, entry = {}) {
|
|
@@ -10316,6 +10407,35 @@ function recordClickUpCommentVersionProcessed({ ledgerPath, key, taskId, eventTy
|
|
|
10316
10407
|
recordedAt: at.toISOString()
|
|
10317
10408
|
});
|
|
10318
10409
|
}
|
|
10410
|
+
function clickUpWorktreeFailureCommentKey({ taskId, message } = {}) {
|
|
10411
|
+
const hash = crypto.createHash("sha256").update(String(message || "")).digest("hex").slice(0, 16);
|
|
10412
|
+
return [String(taskId || "unknown").trim() || "unknown", "worktree_failure", hash].join(":");
|
|
10413
|
+
}
|
|
10414
|
+
function shouldPostClickUpWorktreeFailureComment({ ledgerPath, taskId, message, now = /* @__PURE__ */ new Date(), windowMs = CLICKUP_WORKTREE_FAILURE_COMMENT_DEDUPE_MS } = {}) {
|
|
10415
|
+
const key = clickUpWorktreeFailureCommentKey({ taskId, message });
|
|
10416
|
+
const nowMs = now instanceof Date ? now.getTime() : new Date(now).getTime();
|
|
10417
|
+
for (const entry of readClickUpCommentLedgerEntries(ledgerPath).reverse()) {
|
|
10418
|
+
if (entry?.key !== key) continue;
|
|
10419
|
+
const postedAtMs = new Date(entry.postedAt || entry.recordedAt || entry.at || 0).getTime();
|
|
10420
|
+
if (Number.isFinite(postedAtMs) && Number.isFinite(nowMs) && nowMs - postedAtMs <= windowMs) {
|
|
10421
|
+
return { post: false, key, reason: "recent_duplicate" };
|
|
10422
|
+
}
|
|
10423
|
+
if (entry?.message === message) return { post: false, key, reason: "latest_equivalent" };
|
|
10424
|
+
}
|
|
10425
|
+
return { post: true, key };
|
|
10426
|
+
}
|
|
10427
|
+
function recordClickUpWorktreeFailureComment({ ledgerPath, taskId, message, posted = true, skippedReason = null, at = /* @__PURE__ */ new Date() } = {}) {
|
|
10428
|
+
appendClickUpCommentLedgerEntry(ledgerPath, {
|
|
10429
|
+
key: clickUpWorktreeFailureCommentKey({ taskId, message }),
|
|
10430
|
+
taskId,
|
|
10431
|
+
eventType: "worktree_failure",
|
|
10432
|
+
action: posted ? "worktree_failure_comment_posted" : "worktree_failure_comment_skipped",
|
|
10433
|
+
message,
|
|
10434
|
+
posted,
|
|
10435
|
+
skippedReason,
|
|
10436
|
+
postedAt: at.toISOString()
|
|
10437
|
+
});
|
|
10438
|
+
}
|
|
10319
10439
|
function clickUpTaskIdFromPayload(payload = {}) {
|
|
10320
10440
|
return String(payload.task_id || payload.taskId || payload.task?.id || payload.task?.task_id || "").trim();
|
|
10321
10441
|
}
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
- `product_manager` may answer, investigate, operate dashboards, and pre-estimate "a qué huele" small/medium/large plus rough story points; WPM owns delivery routing.
|
|
23
23
|
- Supported delivery task types are `Tarea`, `Bug`, `Doc`, and `PoC`; ignore `Idea`, legacy `Backlog` alias, `Hito`, `Nota de reunión`, and `Respuesta del formulario` unless converted or linked to delivery work.
|
|
24
24
|
- Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if a task worktree lacks that file, use the Optima-provided human role fallback context and configured ClickUp IDs instead of blocking solely on the missing repo-local file. Use role identifiers in workflow text and automation config.
|
|
25
|
-
- Status actions are deterministic: `backlog` ignore, `plan` plan plus `Story Points`, test strategy, `Definition`,
|
|
25
|
+
- Status actions are deterministic: `backlog` ignore, `plan` plan plus `Story Points`, test strategy, and `Definition`, `in progress` execute, `validation` split Tech Lead and Validator/QA gates, `merge` parent post-approval automation, and `completed`/`Closed` ignore unless reopened.
|
|
26
|
+
- Human approval assignment is prohibited except for the strict allowlist: parent `plan` with clear questions already posted in ClickUp comments; `in progress` blocked by missing credentials, permissions, external tools, or access; or parent `validation` with a functional preview URL such as `https://<taskid>-preview.defend.tech`. Do not assign `CTO`/`PO` for generic handoff, routine validation, cleanup, subtask planning/validation, or partial-phase stops.
|
|
26
27
|
- Store ClickUp `agent_metadata` JSON with session IDs per agent/type/task/subtask; keep `Definition` as the plan contract and final Documentation as delivered behavior docs.
|
|
27
28
|
- `workflow_product_manager` is registered only when explicit ClickUp webhook mode is configured and the local webhook subscription state is active/valid.
|
|
28
29
|
- Webhook mode is opt-in: Optima validates signed `X-Signature` HMAC SHA-256 requests, routes status/assignee events only for Product Manager-assigned non-terminal tasks, routes comments only when they mention `@Defend Tech Product Manager`, stores new `ses_...` ids in ClickUp `agent_metadata`, and reports stale/missing sessions to ClickUp without creating replacements.
|
|
@@ -35,8 +36,8 @@
|
|
|
35
36
|
- Parent task start pulls remote once; after branch creation, subtasks trust the parent local branch instead of continuous remote polling.
|
|
36
37
|
- Parent branch format is `<clickup-task-type>/<parent-task-id>`; subtask branch format is the non-nested sibling ref `<clickup-task-type>/<parent-task-id>-subtask-<subtask-id>`; pending planned subtasks use `<clickup-task-type>/<parent-task-id>-pending-<title-slug>`; PoC branch format is always `poc/<clickup-task-id>` and stays there until a later productization task.
|
|
37
38
|
- Subtask worktrees start from the parent branch and PR to the parent branch; if the parent branch/worktree is missing, bootstrap the parent from `dev`/`origin/dev` first. Parent task PRs target `dev`, and release PRs target `main` from `dev` only after explicit approval.
|
|
38
|
-
- After successful subtask validation, Validator/QA merges the subtask PR into the parent branch/workspace without `CTO`/`PO` approval.
|
|
39
|
-
- After parent Tech Lead and Validator/QA validation passes, the parent task
|
|
39
|
+
- After successful subtask validation, Validator/QA merges the subtask PR into the parent branch/workspace without `CTO`/`PO` assignment or approval.
|
|
40
|
+
- After parent Tech Lead and Validator/QA validation passes, the parent task may assign `CTO`/`PO` only when a functional validation URL is provided; after a human comments `Approved`, automation removes human assignees, assigns itself or the merge owner, merges the parent PR into `dev`, cleans workspaces/worktrees/branches, pushes to `dev`, and ensures the dev environment contains the code.
|
|
40
41
|
- If any subtask or parent merge conflicts or fails, Validator/QA returns the affected ClickUp item to `in progress` and routes it to the coding owner.
|
|
41
42
|
- Never push directly to `main`.
|
|
42
43
|
- `investigation` and `spec` tasks may run in parallel only when they avoid conflicting delivery artifacts.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- `product_manager` may answer/investigate/dashboard/pre-estimate "a qué huele" plus rough story points; development asks become routed ClickUp tasks.
|
|
15
15
|
- ClickUp-first types: execute `Tarea`, `Bug`, `Doc`, `PoC`; ignore `Idea`, legacy `Backlog` alias, `Hito`, `Nota de reunión`, `Respuesta del formulario` unless converted/linked.
|
|
16
16
|
- Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if missing in a task worktree, use Optima-provided fallback role context/configured ClickUp IDs instead of blocking solely on the missing file.
|
|
17
|
-
- ClickUp-first statuses: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, test strategy
|
|
17
|
+
- ClickUp-first statuses: `backlog` ignore, `plan` plan with `Story Points`, `Definition`, and test strategy; assign `CTO`/`PO` only for parent `plan` questions with clear ClickUp comments, real `in progress` blockers from missing credentials/tools/access, or parent `validation` with a functional preview URL. Subtasks merge directly into the parent branch after Validator/QA passes without CTO/PO assignment; parent `Approved` comments trigger automation to remove humans, assign merge owner/self, merge to `dev`, clean workspaces/worktrees/branches, push, and ensure dev receives the code; `completed`/`Closed` ignore unless reopened.
|
|
18
18
|
- Shared-worktree rule: one active `implementation` task at a time; isolated `investigation`/`spec` may run in parallel if non-conflicting.
|
|
19
19
|
- Git rules: principal workspace stays on `dev`, never `main`; parent branches use `<type>/<parent-id>`; subtask branches use non-nested `<type>/<parent-id>-subtask-<subtask-id>` and pending subtasks use `<type>/<parent-id>-pending-<title-slug>`; parent task pulls remote once at start; subtasks start from and PR to the parent local branch, bootstrapping the parent from `dev`/`origin/dev` first when missing; PoC branches stay `poc/<clickup-task-id>`; parents PR to `dev`, releases PR `dev` -> `main`; failed/conflicted subtask or parent merges return the affected item to `in progress` for the coding owner; no direct `main` pushes.
|
|
20
20
|
- Store `agent_metadata` session JSON; `Definition` is the plan contract, final Documentation is delivered behavior docs.
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
- Raw logs stay in evidence storage; ClickUp receives concise summaries, paths/links, or relevant excerpts only, never wholesale raw logs.
|
|
22
22
|
- WPM owns ClickUp `Story Points` during `plan`, re-estimation on material plan changes, `agent_metadata` session JSON, `Definition` plan-contract linking, and parent approval routing after validation.
|
|
23
23
|
- Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if a task worktree lacks that file, use Optima-provided fallback role context and configured ClickUp IDs instead of blocking solely on the missing repo-local file. Use role identifiers in workflow text and automation config.
|
|
24
|
-
-
|
|
25
|
-
-
|
|
24
|
+
- Human approval assignment is prohibited except for three cases: parent `plan` with clear ClickUp-commented questions, `in progress` blockers caused by missing credentials/permissions/tools/access, or parent `validation` with a functional preview URL. Do not assign `CTO`/`PO` for generic handoff, routine validation, cleanup, subtask work, or partial-phase stops.
|
|
25
|
+
- Subtask merge authority belongs to Validator/QA after successful subtask validation: subtask PRs target and merge into the parent branch/workspace without `CTO`/`PO` assignment or approval.
|
|
26
|
+
- Parent merge authority is split: after Tech Lead and Validator/QA pass, WPM/Validator may assign `CTO`/`PO` only under the parent-validation allowlist; after a human comments `Approved`, automation removes human assignees, assigns itself or the merge owner, merges to `dev`, cleans workspaces/worktrees/branches, pushes to `dev`, and ensures the dev environment contains the code.
|
|
26
27
|
- If a subtask or parent merge conflicts or fails, Validator/QA returns the affected ClickUp task/subtask to `in progress` and routes it back to the coding owner.
|
|
27
28
|
- Git authority follows ClickUp-first rules: principal workspace on `dev`, no direct `main` push, parent task pulls remote once at start, subtask PRs to parent branch, parent PRs to `dev`, PoC branches stay `poc/<clickup-task-id>`, release PRs from `dev` to `main` only after approval.
|
|
28
29
|
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
- Keep raw logs in evidence storage; ClickUp receives concise summaries, paths/links, or relevant excerpts only, never wholesale raw logs.
|
|
16
16
|
- WPM owns `Story Points` during `plan`, re-estimation on material plan changes, `agent_metadata`, `Definition` plan-contract linking, and parent approval routing after validation.
|
|
17
17
|
- Human role registry: resolve `CTO` and `PO` from `docs/core/humans.md` when present; if missing in a task worktree, use Optima fallback role context/configured ClickUp IDs instead of blocking solely on the missing file.
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- Human approval assignment is prohibited except for parent `plan` questions with clear ClickUp comments, `in progress` blockers from missing credentials/tools/access, or parent `validation` with a functional preview URL; never use it for generic handoff, cleanup, subtasks, or phase stops.
|
|
19
|
+
- Validator/QA may merge validated subtask PRs into the parent branch/workspace without `CTO`/`PO` assignment or approval.
|
|
20
|
+
- Parent merge authority uses the validation allowlist only: after a human comments `Approved`, automation removes human assignees, assigns merge owner/self, merges to `dev`, cleans workspaces/worktrees/branches, pushes, and ensures dev receives the code.
|
|
20
21
|
- Failed or conflicted subtask/parent merges return the affected ClickUp item to `in progress` for the coding owner.
|
|
21
22
|
- ClickUp-first Git rules: principal workspace on `dev`, no direct `main` push, parent pulls remote once at start, subtask PRs to parent branch, parent PRs to `dev`, PoC branches stay `poc/<clickup-task-id>`, release PRs `dev` -> `main` only after approval.
|
|
22
23
|
- BA owns product truth and product-facing feature/domain docs.
|