@defend-tech/opencode-optima 0.1.24 → 0.1.26

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.
Files changed (26) hide show
  1. package/README.md +1 -1
  2. package/{agents → assets/agents}/codemap.yml +1 -1
  3. package/{policies → assets/policies}/README.md +2 -2
  4. package/{policies → assets/policies}/codemap.yml +1 -1
  5. package/dist/index.js +91 -7
  6. package/dist/sanitize_cli.js +91 -7
  7. package/docs/setup/CONFIGURATION.md +1 -1
  8. package/package.json +2 -3
  9. /package/{agents → assets/agents}/business_analyst.md +0 -0
  10. /package/{agents → assets/agents}/developer.md +0 -0
  11. /package/{agents → assets/agents}/ops_product_manager.md +0 -0
  12. /package/{agents → assets/agents}/product_manager.md +0 -0
  13. /package/{agents → assets/agents}/qa_engineer.md +0 -0
  14. /package/{agents → assets/agents}/tech_lead.md +0 -0
  15. /package/{agents → assets/agents}/technical_architect.md +0 -0
  16. /package/{agents → assets/agents}/ui_ux_designer.md +0 -0
  17. /package/{agents → assets/agents}/workflow_product_manager.md +0 -0
  18. /package/{agents → assets/agents}/workflow_runner.md +0 -0
  19. /package/{policies → assets/policies}/definition-of-done.md +0 -0
  20. /package/{policies → assets/policies}/definition-of-ready.md +0 -0
  21. /package/{policies → assets/policies}/development-guidelines.md +0 -0
  22. /package/{policies → assets/policies}/documentation-guidelines.md +0 -0
  23. /package/{policies → assets/policies}/git-commit-messaging.md +0 -0
  24. /package/{policies → assets/policies}/product-guidelines.md +0 -0
  25. /package/{policies → assets/policies}/testing-guidelines.md +0 -0
  26. /package/{policies → assets/policies}/ui-ux-guidelines.md +0 -0
package/README.md CHANGED
@@ -25,7 +25,7 @@ PMA will guide the repository setup flow and, when needed, initialize Optima ins
25
25
 
26
26
  During setup, PMA can initialize the repository and create `.optima/.config/optima.yaml`. Optima reads this file for repository-local defaults, feature flags, policy extraction settings, and per-agent config overrides.
27
27
 
28
- Repository-local policy overrides live in `.optima/policies/`. If a policy file is not present there, Optima falls back to the bundled plugin default automatically.
28
+ Repository-local policy overrides live in `.optima/policies/`. If a policy file is not present there, Optima falls back to the bundled plugin default from package `assets/policies/` automatically.
29
29
 
30
30
  Repository-local full agent definitions can live in `.optima/agents/`. Use this folder to override a bundled agent's base prompt or define a brand new custom repository agent.
31
31
 
@@ -1,5 +1,5 @@
1
1
  scope: module
2
- parent: ../.optima/codemap.yml
2
+ parent: ../../.optima/codemap.yml
3
3
  sources_of_truth:
4
4
  - path: product_manager.md
5
5
  - path: workflow_runner.md
@@ -1,13 +1,13 @@
1
1
  # Optima Bundled Policies
2
2
 
3
- This package-root directory contains Optima's bundled fallback policy assets. Target repositories should put repo-local policy overrides in `.optima/policies/`, not in this package-root `policies/` directory.
3
+ This package asset directory contains Optima's bundled fallback policy assets. Target repositories should put repo-local policy overrides in `.optima/policies/`, not in a package-root `policies/` directory.
4
4
 
5
5
  ## How Policy Resolution Works
6
6
 
7
7
  For any `<include:policy:<file>.md>` include, Optima resolves policy files in this order:
8
8
 
9
9
  1. `.optima/policies/<file>.md`
10
- 2. bundled plugin default `policies/<file>.md`
10
+ 2. bundled plugin default from `assets/policies/<file>.md`
11
11
 
12
12
  Files under `.optima/.config/generated/policies/` are reference copies only. They are not read directly at runtime.
13
13
 
@@ -1,5 +1,5 @@
1
1
  scope: module
2
- parent: ../codemap.yml
2
+ parent: ../../.optima/codemap.yml
3
3
  sources_of_truth:
4
4
  - path: README.md
5
5
  - path: definition-of-done.md
package/dist/index.js CHANGED
@@ -7900,8 +7900,9 @@ async function optima_validate_logic(worktree) {
7900
7900
 
7901
7901
  // src/index.js
7902
7902
  var PKG_ROOT = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..");
7903
- var BUNDLE_AGENTS_DIR = path2.join(PKG_ROOT, "agents");
7904
- var BUNDLE_POLICIES_DIR = path2.join(PKG_ROOT, "policies");
7903
+ var BUNDLE_ASSETS_DIR = path2.join(PKG_ROOT, "assets");
7904
+ var BUNDLE_AGENTS_DIR = path2.join(BUNDLE_ASSETS_DIR, "agents");
7905
+ var BUNDLE_POLICIES_DIR = path2.join(BUNDLE_ASSETS_DIR, "policies");
7905
7906
  var TEMPLATES_DIR = path2.join(PKG_ROOT, "templates");
7906
7907
  var HUMANS_REGISTRY_PATH = path2.join(PKG_ROOT, "docs", "core", "humans.md");
7907
7908
  var MANDATORY_AGENTS = /* @__PURE__ */ new Set(["product_manager", "business_analyst", "tech_lead"]);
@@ -7965,7 +7966,7 @@ var REPO_LOCAL_POLICIES_README = [
7965
7966
  "For any `<include:policy:<file>.md>` include, Optima resolves policy files in this order:",
7966
7967
  "",
7967
7968
  "1. `.optima/policies/<file>.md`",
7968
- "2. bundled plugin default `policies/<file>.md`",
7969
+ "2. bundled plugin default from package assets (`assets/policies/<file>.md`)",
7969
7970
  "",
7970
7971
  "Files under `.optima/.config/generated/policies/` are reference copies only. They are not read directly at runtime.",
7971
7972
  "",
@@ -8853,6 +8854,9 @@ function optimaRuntimeDir(worktree) {
8853
8854
  function clickUpWebhookStatePath(worktree) {
8854
8855
  return path2.join(optimaRuntimeDir(worktree), "clickup-webhook.json");
8855
8856
  }
8857
+ function clickUpCommentLedgerPath(worktree) {
8858
+ return path2.join(optimaRuntimeDir(worktree), "clickup-comment-ledger.jsonl");
8859
+ }
8856
8860
  function clickUpWebhookLogPath(worktree) {
8857
8861
  return path2.join(optimaRuntimeDir(worktree), "clickup-webhook.log.jsonl");
8858
8862
  }
@@ -9206,6 +9210,71 @@ function rememberClickUpWebhookEvent(state = {}, eventKey, limit = 200) {
9206
9210
  if (recent.includes(eventKey)) return { duplicate: true, state };
9207
9211
  return { duplicate: false, state: { ...state, recentEventKeys: [...recent, eventKey].slice(-limit) } };
9208
9212
  }
9213
+ function readClickUpCommentLedger(ledgerPath) {
9214
+ const processed = /* @__PURE__ */ new Set();
9215
+ if (!ledgerPath || !fs2.existsSync(ledgerPath)) return processed;
9216
+ try {
9217
+ const raw = fs2.readFileSync(ledgerPath, "utf8");
9218
+ for (const [index, line] of raw.split(/\r?\n/).entries()) {
9219
+ if (!line.trim()) continue;
9220
+ try {
9221
+ const entry = JSON.parse(line);
9222
+ if (entry?.key) processed.add(String(entry.key));
9223
+ } catch (error) {
9224
+ throw new Error(`malformed ledger row ${index + 1}: ${error.message}`);
9225
+ }
9226
+ }
9227
+ } catch (error) {
9228
+ throw new Error(`ClickUp comment ledger unavailable: ${error.message}`);
9229
+ }
9230
+ return processed;
9231
+ }
9232
+ function appendClickUpCommentLedgerEntry(ledgerPath, entry = {}) {
9233
+ if (!ledgerPath || !entry.key) return;
9234
+ fs2.mkdirSync(path2.dirname(ledgerPath), { recursive: true, mode: 448 });
9235
+ fs2.appendFileSync(ledgerPath, `${JSON.stringify(entry)}
9236
+ `, { encoding: "utf8", mode: 384 });
9237
+ try {
9238
+ fs2.chmodSync(ledgerPath, 384);
9239
+ } catch {
9240
+ }
9241
+ }
9242
+ function clickUpCommentLedgerKey({ taskId, eventType, payload }) {
9243
+ const history = Array.isArray(payload?.history_items) ? payload.history_items[0] : payload?.history_item;
9244
+ const comment = clickUpCommentFromPayload(payload);
9245
+ const commentId = String(comment?.id || payload?.comment_id || payload?.commentId || history?.comment_id || history?.commentId || "").trim();
9246
+ if (!commentId) return "";
9247
+ const explicitVersion = String(
9248
+ comment?.date_updated || comment?.dateUpdated || comment?.updated_at || comment?.updatedAt || comment?._version_vector || comment?.version_vector || comment?.versionVector || comment?.version || comment?.revision || comment?.modified_at || comment?.modifiedAt || ""
9249
+ ).trim();
9250
+ const contentVersion = crypto.createHash("sha256").update(JSON.stringify({ text: clickUpCommentText(comment), parts: Array.isArray(comment.comment) ? comment.comment : null })).digest("hex").slice(0, 16);
9251
+ return [String(taskId || "").trim(), "comment", commentId, explicitVersion || `sha256-${contentVersion}`].filter(Boolean).join(":");
9252
+ }
9253
+ function isClickUpCommentVersionProcessed({ ledgerPath, key, ledger = null, worktree = process.cwd() } = {}) {
9254
+ if (!key) return false;
9255
+ try {
9256
+ return (ledger || readClickUpCommentLedger(ledgerPath)).has(key);
9257
+ } catch (error) {
9258
+ try {
9259
+ appendClickUpWebhookLocalLog(worktree, { type: "comment_ledger_read_failed", ledgerPath, message: error.message });
9260
+ } catch {
9261
+ }
9262
+ throw error;
9263
+ }
9264
+ }
9265
+ function recordClickUpCommentVersionProcessed({ ledgerPath, key, taskId, eventType, payload, result, at = /* @__PURE__ */ new Date() } = {}) {
9266
+ if (!key) return;
9267
+ const comment = clickUpCommentFromPayload(payload);
9268
+ appendClickUpCommentLedgerEntry(ledgerPath, {
9269
+ key,
9270
+ taskId,
9271
+ eventType,
9272
+ commentId: comment?.id || payload?.comment_id || payload?.commentId || null,
9273
+ action: result?.action || null,
9274
+ sessionId: result?.sessionId || null,
9275
+ recordedAt: at.toISOString()
9276
+ });
9277
+ }
9209
9278
  function clickUpTaskIdFromPayload(payload = {}) {
9210
9279
  return String(payload.task_id || payload.taskId || payload.task?.id || payload.task?.task_id || "").trim();
9211
9280
  }
@@ -9235,12 +9304,15 @@ function clickUpCommentAuthorId(comment = {}) {
9235
9304
  return String(comment.user?.id || comment.user?.userid || comment.author?.id || comment.author_id || comment.user_id || "").trim();
9236
9305
  }
9237
9306
  function clickUpCommentText(comment = {}) {
9238
- return String(comment.comment_text || comment.text || comment.comment || comment.plain_text || "");
9307
+ if (Array.isArray(comment.comment)) return comment.comment.map((part) => String(part?.text || "")).join("");
9308
+ return String(comment.comment_text || comment.text || comment.comment || comment.plain_text || comment.text_content || "");
9239
9309
  }
9240
9310
  function clickUpCommentMentionsProductManager(comment = {}, routing = {}) {
9241
9311
  const mentionUserId = String(routing.productManagerMentionUserId || "").trim();
9242
9312
  const mentions = Array.isArray(comment.mentions) ? comment.mentions : [];
9243
9313
  if (mentionUserId && mentions.some((mention) => String(mention?.id || mention?.userid || mention?.user?.id || mention).trim() === mentionUserId)) return true;
9314
+ const richText = Array.isArray(comment.comment) ? comment.comment : [];
9315
+ if (mentionUserId && richText.some((part) => part?.type === "tag" && String(part?.user?.id || part?.user?.userid || "").trim() === mentionUserId)) return true;
9244
9316
  const fallback = String(routing.productManagerMentionName || CLICKUP_PM_MENTION_NAME).trim().toLowerCase();
9245
9317
  return fallback ? clickUpCommentText(comment).toLowerCase().includes(`@${fallback}`) : false;
9246
9318
  }
@@ -9558,17 +9630,21 @@ async function withClickUpTaskRouteLock(taskId, operation) {
9558
9630
  if (activeClickUpTaskRoutes.get(key) === current) activeClickUpTaskRoutes.delete(key);
9559
9631
  }
9560
9632
  }
9561
- async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, saveState = null, now = () => /* @__PURE__ */ new Date(), host = os.hostname() } = {}) {
9633
+ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, saveState = null, now = () => /* @__PURE__ */ new Date(), host = os.hostname(), commentLedgerPath = clickUpCommentLedgerPath(worktree) } = {}) {
9562
9634
  const eventType = clickUpEventType(payload);
9563
9635
  const eventKey = clickUpWebhookEventKey(payload);
9564
9636
  const remembered = rememberClickUpWebhookEvent(state, eventKey);
9565
9637
  if (remembered.duplicate) return { ok: true, action: "ignored", reason: "duplicate", eventKey };
9566
9638
  let stateToPersist = remembered.state;
9639
+ let commentLedgerKey = "";
9567
9640
  const persistState = (nextState) => {
9568
9641
  stateToPersist = nextState;
9569
9642
  if (saveState) saveState(nextState);
9570
9643
  };
9571
9644
  const finish = (result) => {
9645
+ if (result?.ok && commentLedgerKey && ["created_session", "sent_to_existing_session", "missing_session_reported"].includes(result.action)) {
9646
+ recordClickUpCommentVersionProcessed({ ledgerPath: commentLedgerPath, key: commentLedgerKey, taskId: result.taskId, eventType, payload, result, at: now() });
9647
+ }
9572
9648
  if (result?.ok && saveState) saveState(stateToPersist);
9573
9649
  return result;
9574
9650
  };
@@ -9583,8 +9659,16 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
9583
9659
  const authorId = clickUpCommentAuthorId(comment);
9584
9660
  if (authorId && authorId === config.routing.ignoredCommentAuthorId) return finish({ ok: true, action: "ignored", reason: "self_authored_comment", taskId });
9585
9661
  if (!clickUpCommentMentionsProductManager(comment, config.routing)) return finish({ ok: true, action: "ignored", reason: "missing_product_manager_mention", taskId });
9662
+ commentLedgerKey = clickUpCommentLedgerKey({ taskId, eventType, payload });
9663
+ try {
9664
+ if (isClickUpCommentVersionProcessed({ ledgerPath: commentLedgerPath, key: commentLedgerKey, worktree })) {
9665
+ return finish({ ok: true, action: "ignored", reason: "comment_version_already_processed", taskId, eventKey, commentLedgerKey });
9666
+ }
9667
+ } catch (error) {
9668
+ return { ok: false, action: "error", reason: "comment_ledger_unavailable", taskId, eventKey, message: error.message };
9669
+ }
9586
9670
  }
9587
- if (!isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) return finish({ ok: true, action: "ignored", reason: "not_assigned_to_product_manager", taskId });
9671
+ if (!isCommentEvent && !isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) return finish({ ok: true, action: "ignored", reason: "not_assigned_to_product_manager", taskId });
9588
9672
  if (isClickUpTaskTerminal(task, payload, config.routing.ignoredStatuses)) return finish({ ok: true, action: "ignored", reason: "terminal_status", taskId });
9589
9673
  if (clickupClient?.getTask) {
9590
9674
  const latestTask = await clickupClient.getTask(taskId);
@@ -11394,7 +11478,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
11394
11478
  }
11395
11479
  };
11396
11480
  }
11397
- OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, createClickUpApiClient, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, readClickUpWebhookState, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
11481
+ OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, createClickUpApiClient, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, readClickUpCommentLedger, readClickUpWebhookState, recordClickUpCommentVersionProcessed, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
11398
11482
  export {
11399
11483
  OptimaPlugin as default
11400
11484
  };
@@ -7907,8 +7907,9 @@ async function optima_validate_logic(worktree) {
7907
7907
 
7908
7908
  // src/index.js
7909
7909
  var PKG_ROOT = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..");
7910
- var BUNDLE_AGENTS_DIR = path2.join(PKG_ROOT, "agents");
7911
- var BUNDLE_POLICIES_DIR = path2.join(PKG_ROOT, "policies");
7910
+ var BUNDLE_ASSETS_DIR = path2.join(PKG_ROOT, "assets");
7911
+ var BUNDLE_AGENTS_DIR = path2.join(BUNDLE_ASSETS_DIR, "agents");
7912
+ var BUNDLE_POLICIES_DIR = path2.join(BUNDLE_ASSETS_DIR, "policies");
7912
7913
  var TEMPLATES_DIR = path2.join(PKG_ROOT, "templates");
7913
7914
  var HUMANS_REGISTRY_PATH = path2.join(PKG_ROOT, "docs", "core", "humans.md");
7914
7915
  var MANDATORY_AGENTS = /* @__PURE__ */ new Set(["product_manager", "business_analyst", "tech_lead"]);
@@ -7972,7 +7973,7 @@ var REPO_LOCAL_POLICIES_README = [
7972
7973
  "For any `<include:policy:<file>.md>` include, Optima resolves policy files in this order:",
7973
7974
  "",
7974
7975
  "1. `.optima/policies/<file>.md`",
7975
- "2. bundled plugin default `policies/<file>.md`",
7976
+ "2. bundled plugin default from package assets (`assets/policies/<file>.md`)",
7976
7977
  "",
7977
7978
  "Files under `.optima/.config/generated/policies/` are reference copies only. They are not read directly at runtime.",
7978
7979
  "",
@@ -8860,6 +8861,9 @@ function optimaRuntimeDir(worktree) {
8860
8861
  function clickUpWebhookStatePath(worktree) {
8861
8862
  return path2.join(optimaRuntimeDir(worktree), "clickup-webhook.json");
8862
8863
  }
8864
+ function clickUpCommentLedgerPath(worktree) {
8865
+ return path2.join(optimaRuntimeDir(worktree), "clickup-comment-ledger.jsonl");
8866
+ }
8863
8867
  function clickUpWebhookLogPath(worktree) {
8864
8868
  return path2.join(optimaRuntimeDir(worktree), "clickup-webhook.log.jsonl");
8865
8869
  }
@@ -9213,6 +9217,71 @@ function rememberClickUpWebhookEvent(state = {}, eventKey, limit = 200) {
9213
9217
  if (recent.includes(eventKey)) return { duplicate: true, state };
9214
9218
  return { duplicate: false, state: { ...state, recentEventKeys: [...recent, eventKey].slice(-limit) } };
9215
9219
  }
9220
+ function readClickUpCommentLedger(ledgerPath) {
9221
+ const processed = /* @__PURE__ */ new Set();
9222
+ if (!ledgerPath || !fs2.existsSync(ledgerPath)) return processed;
9223
+ try {
9224
+ const raw = fs2.readFileSync(ledgerPath, "utf8");
9225
+ for (const [index, line] of raw.split(/\r?\n/).entries()) {
9226
+ if (!line.trim()) continue;
9227
+ try {
9228
+ const entry = JSON.parse(line);
9229
+ if (entry?.key) processed.add(String(entry.key));
9230
+ } catch (error) {
9231
+ throw new Error(`malformed ledger row ${index + 1}: ${error.message}`);
9232
+ }
9233
+ }
9234
+ } catch (error) {
9235
+ throw new Error(`ClickUp comment ledger unavailable: ${error.message}`);
9236
+ }
9237
+ return processed;
9238
+ }
9239
+ function appendClickUpCommentLedgerEntry(ledgerPath, entry = {}) {
9240
+ if (!ledgerPath || !entry.key) return;
9241
+ fs2.mkdirSync(path2.dirname(ledgerPath), { recursive: true, mode: 448 });
9242
+ fs2.appendFileSync(ledgerPath, `${JSON.stringify(entry)}
9243
+ `, { encoding: "utf8", mode: 384 });
9244
+ try {
9245
+ fs2.chmodSync(ledgerPath, 384);
9246
+ } catch {
9247
+ }
9248
+ }
9249
+ function clickUpCommentLedgerKey({ taskId, eventType, payload }) {
9250
+ const history = Array.isArray(payload?.history_items) ? payload.history_items[0] : payload?.history_item;
9251
+ const comment = clickUpCommentFromPayload(payload);
9252
+ const commentId = String(comment?.id || payload?.comment_id || payload?.commentId || history?.comment_id || history?.commentId || "").trim();
9253
+ if (!commentId) return "";
9254
+ const explicitVersion = String(
9255
+ comment?.date_updated || comment?.dateUpdated || comment?.updated_at || comment?.updatedAt || comment?._version_vector || comment?.version_vector || comment?.versionVector || comment?.version || comment?.revision || comment?.modified_at || comment?.modifiedAt || ""
9256
+ ).trim();
9257
+ const contentVersion = crypto.createHash("sha256").update(JSON.stringify({ text: clickUpCommentText(comment), parts: Array.isArray(comment.comment) ? comment.comment : null })).digest("hex").slice(0, 16);
9258
+ return [String(taskId || "").trim(), "comment", commentId, explicitVersion || `sha256-${contentVersion}`].filter(Boolean).join(":");
9259
+ }
9260
+ function isClickUpCommentVersionProcessed({ ledgerPath, key, ledger = null, worktree = process.cwd() } = {}) {
9261
+ if (!key) return false;
9262
+ try {
9263
+ return (ledger || readClickUpCommentLedger(ledgerPath)).has(key);
9264
+ } catch (error) {
9265
+ try {
9266
+ appendClickUpWebhookLocalLog(worktree, { type: "comment_ledger_read_failed", ledgerPath, message: error.message });
9267
+ } catch {
9268
+ }
9269
+ throw error;
9270
+ }
9271
+ }
9272
+ function recordClickUpCommentVersionProcessed({ ledgerPath, key, taskId, eventType, payload, result, at = /* @__PURE__ */ new Date() } = {}) {
9273
+ if (!key) return;
9274
+ const comment = clickUpCommentFromPayload(payload);
9275
+ appendClickUpCommentLedgerEntry(ledgerPath, {
9276
+ key,
9277
+ taskId,
9278
+ eventType,
9279
+ commentId: comment?.id || payload?.comment_id || payload?.commentId || null,
9280
+ action: result?.action || null,
9281
+ sessionId: result?.sessionId || null,
9282
+ recordedAt: at.toISOString()
9283
+ });
9284
+ }
9216
9285
  function clickUpTaskIdFromPayload(payload = {}) {
9217
9286
  return String(payload.task_id || payload.taskId || payload.task?.id || payload.task?.task_id || "").trim();
9218
9287
  }
@@ -9242,12 +9311,15 @@ function clickUpCommentAuthorId(comment = {}) {
9242
9311
  return String(comment.user?.id || comment.user?.userid || comment.author?.id || comment.author_id || comment.user_id || "").trim();
9243
9312
  }
9244
9313
  function clickUpCommentText(comment = {}) {
9245
- return String(comment.comment_text || comment.text || comment.comment || comment.plain_text || "");
9314
+ if (Array.isArray(comment.comment)) return comment.comment.map((part) => String(part?.text || "")).join("");
9315
+ return String(comment.comment_text || comment.text || comment.comment || comment.plain_text || comment.text_content || "");
9246
9316
  }
9247
9317
  function clickUpCommentMentionsProductManager(comment = {}, routing = {}) {
9248
9318
  const mentionUserId = String(routing.productManagerMentionUserId || "").trim();
9249
9319
  const mentions = Array.isArray(comment.mentions) ? comment.mentions : [];
9250
9320
  if (mentionUserId && mentions.some((mention) => String(mention?.id || mention?.userid || mention?.user?.id || mention).trim() === mentionUserId)) return true;
9321
+ const richText = Array.isArray(comment.comment) ? comment.comment : [];
9322
+ if (mentionUserId && richText.some((part) => part?.type === "tag" && String(part?.user?.id || part?.user?.userid || "").trim() === mentionUserId)) return true;
9251
9323
  const fallback = String(routing.productManagerMentionName || CLICKUP_PM_MENTION_NAME).trim().toLowerCase();
9252
9324
  return fallback ? clickUpCommentText(comment).toLowerCase().includes(`@${fallback}`) : false;
9253
9325
  }
@@ -9565,17 +9637,21 @@ async function withClickUpTaskRouteLock(taskId, operation) {
9565
9637
  if (activeClickUpTaskRoutes.get(key) === current) activeClickUpTaskRoutes.delete(key);
9566
9638
  }
9567
9639
  }
9568
- async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, saveState = null, now = () => /* @__PURE__ */ new Date(), host = os.hostname() } = {}) {
9640
+ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, worktree = process.cwd(), clickupClient, openCodeClient, sessionExists = openCodeSessionExists, createSession = createOpenCodeSession, sendSessionEvent = sendOpenCodeSessionEvent, saveState = null, now = () => /* @__PURE__ */ new Date(), host = os.hostname(), commentLedgerPath = clickUpCommentLedgerPath(worktree) } = {}) {
9569
9641
  const eventType = clickUpEventType(payload);
9570
9642
  const eventKey = clickUpWebhookEventKey(payload);
9571
9643
  const remembered = rememberClickUpWebhookEvent(state, eventKey);
9572
9644
  if (remembered.duplicate) return { ok: true, action: "ignored", reason: "duplicate", eventKey };
9573
9645
  let stateToPersist = remembered.state;
9646
+ let commentLedgerKey = "";
9574
9647
  const persistState = (nextState) => {
9575
9648
  stateToPersist = nextState;
9576
9649
  if (saveState) saveState(nextState);
9577
9650
  };
9578
9651
  const finish = (result) => {
9652
+ if (result?.ok && commentLedgerKey && ["created_session", "sent_to_existing_session", "missing_session_reported"].includes(result.action)) {
9653
+ recordClickUpCommentVersionProcessed({ ledgerPath: commentLedgerPath, key: commentLedgerKey, taskId: result.taskId, eventType, payload, result, at: now() });
9654
+ }
9579
9655
  if (result?.ok && saveState) saveState(stateToPersist);
9580
9656
  return result;
9581
9657
  };
@@ -9590,8 +9666,16 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
9590
9666
  const authorId = clickUpCommentAuthorId(comment);
9591
9667
  if (authorId && authorId === config.routing.ignoredCommentAuthorId) return finish({ ok: true, action: "ignored", reason: "self_authored_comment", taskId });
9592
9668
  if (!clickUpCommentMentionsProductManager(comment, config.routing)) return finish({ ok: true, action: "ignored", reason: "missing_product_manager_mention", taskId });
9669
+ commentLedgerKey = clickUpCommentLedgerKey({ taskId, eventType, payload });
9670
+ try {
9671
+ if (isClickUpCommentVersionProcessed({ ledgerPath: commentLedgerPath, key: commentLedgerKey, worktree })) {
9672
+ return finish({ ok: true, action: "ignored", reason: "comment_version_already_processed", taskId, eventKey, commentLedgerKey });
9673
+ }
9674
+ } catch (error) {
9675
+ return { ok: false, action: "error", reason: "comment_ledger_unavailable", taskId, eventKey, message: error.message };
9676
+ }
9593
9677
  }
9594
- if (!isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) return finish({ ok: true, action: "ignored", reason: "not_assigned_to_product_manager", taskId });
9678
+ if (!isCommentEvent && !isClickUpTaskAssignedToProductManager(task, config.routing.productManagerAssigneeId)) return finish({ ok: true, action: "ignored", reason: "not_assigned_to_product_manager", taskId });
9595
9679
  if (isClickUpTaskTerminal(task, payload, config.routing.ignoredStatuses)) return finish({ ok: true, action: "ignored", reason: "terminal_status", taskId });
9596
9680
  if (clickupClient?.getTask) {
9597
9681
  const latestTask = await clickupClient.getTask(taskId);
@@ -11401,7 +11485,7 @@ Follow-up: use optima_prompt_workflow with session_id '${sessionId}' to check in
11401
11485
  }
11402
11486
  };
11403
11487
  }
11404
- OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, createClickUpApiClient, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, readClickUpWebhookState, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
11488
+ OptimaPlugin.__internals = { BUNDLE_AGENTS_DIR, BUNDLE_ASSETS_DIR, CLICKUP_DEFINITION_DOC_PARENT, activeClickUpWebhookLifecycleRegistry, cleanupManagedClickUpWebhook, deleteClickUpWebhookBestEffort, BUNDLE_POLICIES_DIR, buildClickUpApplyPayloadResult, buildClickUpCreateSubtasksPayload, buildClickUpStartTaskPayload, buildClickUpSummaryPayload, buildClickUpTransitionPayload, buildOptimaAgents, clickUpCommentLedgerKey, clickUpCommentMentionsProductManager, clickUpWebhookAuditLogDir, clickUpWebhookAuditLogPath, createClickUpApiClient, ensureClickUpWebhookSubscription, handleClickUpWebhookRequest, isClickUpWebhookStateActive, normalizeClickUpWebhookConfig, normalizeClickUpWebhookLogLevel, normalizeOpenCodeBaseUrl, readClickUpCommentLedger, readClickUpWebhookState, recordClickUpCommentVersionProcessed, resolveOptimaPluginOptions, resolveSecretReference, routeClickUpWebhookEvent, sendOpenCodeSessionEvent, sendOpenCodeSessionEventDirect, startClickUpWebhookListener, validateClickUpWebhookState, verifyClickUpSignature, writeClickUpWebhookState, decideClickUpStatusAction, deriveClickUpBranchName, deriveClickUpPendingSubtaskBranch, deriveClickUpPrTarget, deriveClickUpWorktree, determineClickUpMergeAuthority, ensureOptimaGitignoreRules, explicitSafeInputWorktree, finalApprovalAssignees, formatValidationResult, isIgnoredClickUpTaskType, isOptimaPluginPackageWorktree, isSafeWritableDirectory, loadHumansRegistry, mergeClickUpAgentMetadata, mergeClickUpSessionMetadata, migrateLegacyOptimaLayout, normalizeAgentMetadataJson, normalizeClickUpStatus, normalizeClickUpTaskType, normalizePromptResponseParts, normalizeWorkflowTaskPath, parseClickUpSubtasksMarkdown, parseHumansRegistry, parseMarkdownArtifact, parseMarkdownSections, preEstimateClickUpWork, readMarkdownArtifact, resolveHumanRoles, resolveSafeWorktree, safeWorktreeOrFailure, stripRawLogSections, validateMainWorkspaceBranchSafety, workflowFinalMessageFromPromptResponse };
11405
11489
 
11406
11490
  // src/sanitize_cli.js
11407
11491
  var { migrateLegacyOptimaLayout: migrateLegacyOptimaLayout2 } = OptimaPlugin.__internals;
@@ -90,7 +90,7 @@ policies:
90
90
  extract_defaults: all
91
91
  ```
92
92
 
93
- This writes the bundled default policy files to `.optima/.config/generated/policies/` for reference. Those generated files are not used directly at runtime. To customize one, copy it into `.optima/policies/` and edit the copy.
93
+ This writes the bundled default policy files from package `assets/policies/` to `.optima/.config/generated/policies/` for reference. Those generated files are not used directly at runtime. To customize one, copy it into `.optima/policies/` and edit the copy.
94
94
 
95
95
  ### Add repository-specific agent instructions
96
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defend-tech/opencode-optima",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+ssh://git@github.com/defend-tech/opencode-optima.git"
@@ -25,9 +25,8 @@
25
25
  },
26
26
  "files": [
27
27
  "dist",
28
- "agents",
28
+ "assets",
29
29
  "docs",
30
- "policies",
31
30
  "templates",
32
31
  "scripts/optima-sanitize-host.js",
33
32
  "Agents_Common.md",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes