@useorgx/openclaw-plugin 0.4.5 → 0.4.6

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 (54) hide show
  1. package/README.md +24 -3
  2. package/dashboard/dist/assets/0tOC3wSN.js +214 -0
  3. package/dashboard/dist/assets/B3ziCA02.js +8 -0
  4. package/dashboard/dist/assets/Bm8QnMJ_.js +1 -0
  5. package/dashboard/dist/assets/CpJsfbXo.js +9 -0
  6. package/dashboard/dist/assets/CyxZio4Y.js +1 -0
  7. package/dashboard/dist/assets/DaAIOik3.css +1 -0
  8. package/dashboard/dist/assets/sAhvFnpk.js +4 -0
  9. package/dashboard/dist/index.html +5 -5
  10. package/dist/activity-store.d.ts +28 -0
  11. package/dist/activity-store.js +250 -0
  12. package/dist/agent-context-store.d.ts +19 -0
  13. package/dist/agent-context-store.js +60 -3
  14. package/dist/agent-suite.d.ts +83 -0
  15. package/dist/agent-suite.js +615 -0
  16. package/dist/contracts/client.d.ts +22 -1
  17. package/dist/contracts/client.js +120 -3
  18. package/dist/contracts/types.d.ts +190 -1
  19. package/dist/entity-comment-store.d.ts +29 -0
  20. package/dist/entity-comment-store.js +190 -0
  21. package/dist/hooks/post-reporting-event.mjs +326 -0
  22. package/dist/http-handler.d.ts +7 -1
  23. package/dist/http-handler.js +3603 -578
  24. package/dist/index.js +936 -62
  25. package/dist/mcp-client-setup.js +156 -24
  26. package/dist/mcp-http-handler.d.ts +17 -0
  27. package/dist/mcp-http-handler.js +144 -3
  28. package/dist/next-up-queue-store.d.ts +31 -0
  29. package/dist/next-up-queue-store.js +169 -0
  30. package/dist/openclaw.plugin.json +1 -1
  31. package/dist/outbox.d.ts +1 -1
  32. package/dist/runtime-instance-store.d.ts +1 -1
  33. package/dist/runtime-instance-store.js +20 -3
  34. package/dist/skill-pack-state.d.ts +69 -0
  35. package/dist/skill-pack-state.js +232 -0
  36. package/dist/worker-supervisor.d.ts +25 -0
  37. package/dist/worker-supervisor.js +62 -0
  38. package/openclaw.plugin.json +1 -1
  39. package/package.json +10 -1
  40. package/skills/orgx-design-agent/SKILL.md +38 -0
  41. package/skills/orgx-engineering-agent/SKILL.md +55 -0
  42. package/skills/orgx-marketing-agent/SKILL.md +40 -0
  43. package/skills/orgx-operations-agent/SKILL.md +40 -0
  44. package/skills/orgx-orchestrator-agent/SKILL.md +45 -0
  45. package/skills/orgx-product-agent/SKILL.md +39 -0
  46. package/skills/orgx-sales-agent/SKILL.md +40 -0
  47. package/skills/ship/SKILL.md +63 -0
  48. package/dashboard/dist/assets/B68j2crt.js +0 -1
  49. package/dashboard/dist/assets/BZZ-fiJx.js +0 -32
  50. package/dashboard/dist/assets/BoXlCHKa.js +0 -9
  51. package/dashboard/dist/assets/Bq9x_Xyh.css +0 -1
  52. package/dashboard/dist/assets/DBhrRVdp.js +0 -1
  53. package/dashboard/dist/assets/DD1jv1Hd.js +0 -8
  54. package/dashboard/dist/assets/DNjbmawF.js +0 -214
package/dist/index.js CHANGED
@@ -12,20 +12,25 @@
12
12
  */
13
13
  import { OrgXClient } from "./api.js";
14
14
  import { createHttpHandler } from "./http-handler.js";
15
- import { readFileSync } from "node:fs";
15
+ import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan } from "./agent-suite.js";
16
+ import { appendActivityItems } from "./activity-store.js";
17
+ import { existsSync, readFileSync } from "node:fs";
16
18
  import { join } from "node:path";
17
19
  import { homedir } from "node:os";
18
20
  import { fileURLToPath } from "node:url";
19
- import { randomUUID } from "node:crypto";
21
+ import { createHash, randomUUID } from "node:crypto";
20
22
  import { clearPersistedApiKey, loadAuthStore, resolveInstallationId, saveAuthStore, } from "./auth-store.js";
21
23
  import { clearPersistedSnapshot, readPersistedSnapshot, writePersistedSnapshot, } from "./snapshot-store.js";
22
24
  import { appendToOutbox, readOutbox, readOutboxSummary, replaceOutbox, } from "./outbox.js";
25
+ import { getAgentContext, readAgentContexts } from "./agent-context-store.js";
26
+ import { readAgentRuns, markAgentRunStopped } from "./agent-run-store.js";
23
27
  import { extractProgressOutboxMessage } from "./reporting/outbox-replay.js";
24
28
  import { ensureGatewayWatchdog } from "./gateway-watchdog.js";
25
- import { createMcpHttpHandler } from "./mcp-http-handler.js";
29
+ import { createMcpHttpHandler, } from "./mcp-http-handler.js";
26
30
  import { autoConfigureDetectedMcpClients } from "./mcp-client-setup.js";
27
31
  import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot } from "./openclaw-settings.js";
28
32
  import { posthogCapture } from "./telemetry/posthog.js";
33
+ import { readSkillPackState, refreshSkillPackState } from "./skill-pack-state.js";
29
34
  export { OrgXClient } from "./api.js";
30
35
  const DEFAULT_BASE_URL = "https://www.useorgx.com";
31
36
  const DEFAULT_DOCS_URL = "https://orgx.mintlify.site/guides/openclaw-plugin-setup";
@@ -186,6 +191,7 @@ function resolveConfig(api, input) {
186
191
  baseUrl,
187
192
  syncIntervalMs: pluginConf.syncIntervalMs ?? 300_000,
188
193
  enabled: pluginConf.enabled ?? true,
194
+ autoInstallAgentSuiteOnConnect: pluginConf.autoInstallAgentSuiteOnConnect ?? true,
189
195
  dashboardEnabled: pluginConf.dashboardEnabled ?? true,
190
196
  installationId: input.installationId,
191
197
  pluginVersion: resolvePluginVersion(),
@@ -268,6 +274,29 @@ function isUuid(value) {
268
274
  return false;
269
275
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
270
276
  }
277
+ function inferReportingInitiativeId(input) {
278
+ const env = pickNonEmptyString(process.env.ORGX_INITIATIVE_ID);
279
+ if (isUuid(env))
280
+ return env;
281
+ const agentId = pickNonEmptyString(input.agent_id, input.agentId);
282
+ if (agentId) {
283
+ const ctx = getAgentContext(agentId);
284
+ const ctxInit = ctx?.initiativeId ?? undefined;
285
+ if (isUuid(ctxInit ?? undefined))
286
+ return ctxInit ?? undefined;
287
+ }
288
+ // Fall back to the most recently updated agent context with a UUID initiative id.
289
+ try {
290
+ const store = readAgentContexts();
291
+ const candidates = Object.values(store.agents ?? {}).filter((ctx) => isUuid(ctx?.initiativeId ?? undefined));
292
+ candidates.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
293
+ const picked = candidates[0]?.initiativeId ?? undefined;
294
+ return isUuid(picked) ? picked : undefined;
295
+ }
296
+ catch {
297
+ return undefined;
298
+ }
299
+ }
271
300
  function toReportingPhase(phase, progressPct) {
272
301
  if (progressPct === 100)
273
302
  return "completed";
@@ -363,11 +392,66 @@ export default function register(api) {
363
392
  pollIntervalMs: null,
364
393
  };
365
394
  let activePairing = null;
366
- const baseApiUrl = config.baseUrl.replace(/\/+$/, "");
395
+ // NOTE: base URL can be updated at runtime (e.g. user edits OpenClaw config). Keep it mutable.
396
+ let baseApiUrl = config.baseUrl.replace(/\/+$/, "");
367
397
  const defaultReportingCorrelationId = pickNonEmptyString(process.env.ORGX_CORRELATION_ID) ??
368
398
  `openclaw-${config.installationId}`;
399
+ function refreshConfigFromSources(input) {
400
+ const allowApiKeyChanges = input?.allowApiKeyChanges !== false;
401
+ const previousApiKey = config.apiKey;
402
+ const previousBaseUrl = config.baseUrl;
403
+ const previousUserId = config.userId;
404
+ const previousDocsUrl = config.docsUrl;
405
+ const previousKeySource = config.apiKeySource;
406
+ const latestPersisted = loadAuthStore();
407
+ const next = resolveConfig(api, {
408
+ installationId: config.installationId,
409
+ persistedApiKey: latestPersisted?.apiKey ?? null,
410
+ persistedUserId: latestPersisted?.userId ?? null,
411
+ });
412
+ const nextApiKey = allowApiKeyChanges ? next.apiKey : previousApiKey;
413
+ const nextUserId = allowApiKeyChanges ? next.userId : previousUserId;
414
+ const changed = nextApiKey !== previousApiKey ||
415
+ next.baseUrl !== previousBaseUrl ||
416
+ nextUserId !== previousUserId ||
417
+ next.docsUrl !== previousDocsUrl ||
418
+ next.apiKeySource !== previousKeySource;
419
+ if (!changed) {
420
+ return false;
421
+ }
422
+ if (allowApiKeyChanges) {
423
+ config.apiKey = nextApiKey;
424
+ config.userId = nextUserId;
425
+ config.apiKeySource = next.apiKeySource;
426
+ }
427
+ config.baseUrl = next.baseUrl;
428
+ config.docsUrl = next.docsUrl;
429
+ baseApiUrl = config.baseUrl.replace(/\/+$/, "");
430
+ client.setCredentials({
431
+ apiKey: config.apiKey,
432
+ userId: config.userId,
433
+ baseUrl: config.baseUrl,
434
+ });
435
+ // Keep onboarding state aligned with what's actually configured (without forcing a status transition).
436
+ updateOnboardingState({
437
+ hasApiKey: Boolean(config.apiKey),
438
+ keySource: config.apiKeySource,
439
+ docsUrl: config.docsUrl,
440
+ installationId: config.installationId,
441
+ });
442
+ api.log?.info?.("[orgx] Config refreshed", {
443
+ reason: input?.reason ?? "runtime_refresh",
444
+ baseUrl: config.baseUrl,
445
+ hasApiKey: Boolean(config.apiKey),
446
+ apiKeySource: config.apiKeySource,
447
+ });
448
+ return true;
449
+ }
369
450
  function resolveReportingContext(input) {
370
- const initiativeId = pickNonEmptyString(input.initiative_id, input.initiativeId, process.env.ORGX_INITIATIVE_ID);
451
+ let initiativeId = pickNonEmptyString(input.initiative_id, input.initiativeId, process.env.ORGX_INITIATIVE_ID);
452
+ if (!isUuid(initiativeId)) {
453
+ initiativeId = inferReportingInitiativeId(input);
454
+ }
371
455
  if (!initiativeId || !isUuid(initiativeId)) {
372
456
  return {
373
457
  ok: false,
@@ -411,6 +495,16 @@ export default function register(api) {
411
495
  return err.message;
412
496
  return typeof err === "string" ? err : "Unexpected error";
413
497
  }
498
+ function stableHash(value) {
499
+ return createHash("sha256").update(value).digest("hex");
500
+ }
501
+ function isAuthFailure(err) {
502
+ const message = toErrorMessage(err).toLowerCase();
503
+ return (message.includes("401") ||
504
+ message.includes("unauthorized") ||
505
+ message.includes("invalid_token") ||
506
+ message.includes("invalid api key"));
507
+ }
414
508
  const registerTool = api.registerTool.bind(api);
415
509
  api.registerTool = (tool, options) => {
416
510
  const toolName = tool.name;
@@ -562,22 +656,47 @@ export default function register(api) {
562
656
  }
563
657
  function buildManualKeyConnectUrl() {
564
658
  try {
565
- return new URL("/settings", baseApiUrl).toString();
659
+ // Deep-link into the Security section where API keys live.
660
+ return new URL("/settings#security", baseApiUrl).toString();
566
661
  }
567
662
  catch {
568
- return "https://www.useorgx.com/settings";
663
+ return "https://www.useorgx.com/settings#security";
569
664
  }
570
665
  }
571
- async function fetchOrgxJson(method, path, body) {
666
+ async function fetchOrgxJson(method, path, body, options) {
572
667
  try {
573
- const response = await fetch(`${baseApiUrl}${path}`, {
574
- method,
575
- headers: {
576
- "Content-Type": "application/json",
577
- },
578
- body: body ? JSON.stringify(body) : undefined,
579
- });
580
- const payload = (await response.json().catch(() => null));
668
+ const controller = new AbortController();
669
+ const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
670
+ ? Math.max(1_000, Math.floor(options.timeoutMs))
671
+ : 12_000;
672
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
673
+ let response;
674
+ let rawText = "";
675
+ try {
676
+ response = await fetch(`${baseApiUrl}${path}`, {
677
+ method,
678
+ signal: controller.signal,
679
+ headers: {
680
+ Accept: "application/json",
681
+ "Content-Type": "application/json",
682
+ },
683
+ body: body ? JSON.stringify(body) : undefined,
684
+ });
685
+ rawText = await response.text().catch(() => "");
686
+ }
687
+ finally {
688
+ clearTimeout(timeout);
689
+ }
690
+ const payload = (() => {
691
+ if (!rawText)
692
+ return null;
693
+ try {
694
+ return JSON.parse(rawText);
695
+ }
696
+ catch {
697
+ return null;
698
+ }
699
+ })();
581
700
  if (!response.ok) {
582
701
  const rawError = payload?.error ?? payload?.message;
583
702
  let errorMessage;
@@ -590,18 +709,62 @@ export default function register(api) {
590
709
  typeof rawError.message === "string") {
591
710
  errorMessage = rawError.message;
592
711
  }
712
+ else if (rawText && rawText.trim().length > 0) {
713
+ // Avoid dumping HTML (Cloudflare / Next.js error pages) into UI; keep it short.
714
+ const sanitized = rawText
715
+ .replace(/\s+/g, " ")
716
+ .replace(/<[^>]+>/g, "")
717
+ .trim();
718
+ errorMessage = sanitized.length > 0 ? sanitized.slice(0, 180) : `OrgX request failed (${response.status})`;
719
+ }
593
720
  else {
594
721
  errorMessage = `OrgX request failed (${response.status})`;
595
722
  }
596
- return { ok: false, status: response.status, error: errorMessage };
723
+ const statusToken = `HTTP ${response.status}`;
724
+ if (response.status &&
725
+ !errorMessage.toLowerCase().includes(statusToken.toLowerCase()) &&
726
+ !errorMessage.includes(`(${response.status})`)) {
727
+ errorMessage = `${errorMessage} (HTTP ${response.status})`;
728
+ }
729
+ const debugParts = [];
730
+ const requestId = response.headers.get("x-request-id");
731
+ const vercelId = response.headers.get("x-vercel-id");
732
+ const cfRay = response.headers.get("cf-ray");
733
+ const clerkStatus = response.headers.get("x-clerk-auth-status");
734
+ const clerkReason = response.headers.get("x-clerk-auth-reason");
735
+ if (requestId)
736
+ debugParts.push(`req=${requestId}`);
737
+ if (vercelId && vercelId !== requestId)
738
+ debugParts.push(`vercel=${vercelId}`);
739
+ if (cfRay)
740
+ debugParts.push(`cf-ray=${cfRay}`);
741
+ if (clerkStatus)
742
+ debugParts.push(`clerk=${clerkStatus}`);
743
+ if (clerkReason)
744
+ debugParts.push(`clerk_reason=${clerkReason}`);
745
+ const debugSuffix = debugParts.length > 0 ? ` (${debugParts.join(", ")})` : "";
746
+ return {
747
+ ok: false,
748
+ status: response.status,
749
+ error: `${errorMessage}${debugSuffix}`,
750
+ };
597
751
  }
598
752
  if (payload?.data !== undefined) {
599
753
  return { ok: true, data: payload.data };
600
754
  }
601
- return { ok: true, data: payload };
755
+ if (payload !== null) {
756
+ return { ok: true, data: payload };
757
+ }
758
+ return { ok: true, data: rawText };
602
759
  }
603
760
  catch (err) {
604
- return { ok: false, status: 0, error: toErrorMessage(err) };
761
+ const message = err &&
762
+ typeof err === "object" &&
763
+ "name" in err &&
764
+ err.name === "AbortError"
765
+ ? `OrgX request timed out (method=${method}, path=${path})`
766
+ : toErrorMessage(err);
767
+ return { ok: false, status: 0, error: message };
605
768
  }
606
769
  }
607
770
  function setRuntimeApiKey(input) {
@@ -664,6 +827,7 @@ export default function register(api) {
664
827
  const probeRemote = input.probeRemote === true;
665
828
  const outbox = await readOutboxSummary();
666
829
  const checks = [];
830
+ refreshConfigFromSources({ reason: "health_check" });
667
831
  const hasApiKey = Boolean(config.apiKey);
668
832
  if (hasApiKey) {
669
833
  checks.push({
@@ -721,7 +885,9 @@ export default function register(api) {
721
885
  else {
722
886
  const startedAt = Date.now();
723
887
  try {
724
- await client.getOrgSnapshot();
888
+ // Avoid probing with /api/client/sync: it's heavier than necessary and can
889
+ // create false negatives during transient server slowness.
890
+ await client.getBillingStatus();
725
891
  remoteReachable = true;
726
892
  remoteLatencyMs = Date.now() - startedAt;
727
893
  checks.push({
@@ -816,8 +982,305 @@ export default function register(api) {
816
982
  .filter(Boolean);
817
983
  return strings.length > 0 ? strings : undefined;
818
984
  }
985
+ function isPidAlive(pid) {
986
+ if (!Number.isFinite(pid) || !pid || pid <= 0)
987
+ return false;
988
+ try {
989
+ process.kill(pid, 0);
990
+ return true;
991
+ }
992
+ catch {
993
+ return false;
994
+ }
995
+ }
996
+ function toFiniteNumber(value) {
997
+ if (typeof value === "number" && Number.isFinite(value))
998
+ return value;
999
+ if (typeof value === "string" && value.trim().length > 0) {
1000
+ const parsed = Number(value);
1001
+ if (Number.isFinite(parsed))
1002
+ return parsed;
1003
+ }
1004
+ return null;
1005
+ }
1006
+ function isSafePathSegment(value) {
1007
+ const normalized = value.trim();
1008
+ if (!normalized || normalized === "." || normalized === "..")
1009
+ return false;
1010
+ if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
1011
+ return false;
1012
+ }
1013
+ if (normalized.includes(".."))
1014
+ return false;
1015
+ return true;
1016
+ }
1017
+ function parseRetroEntityType(value) {
1018
+ if (!value)
1019
+ return undefined;
1020
+ switch (value) {
1021
+ case "initiative":
1022
+ case "workstream":
1023
+ case "milestone":
1024
+ case "task":
1025
+ return value;
1026
+ default:
1027
+ return undefined;
1028
+ }
1029
+ }
1030
+ function readOpenClawSessionSummary(input) {
1031
+ const agentId = input.agentId.trim();
1032
+ const sessionId = input.sessionId.trim();
1033
+ if (!agentId || !sessionId) {
1034
+ return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
1035
+ }
1036
+ if (!isSafePathSegment(agentId) || !isSafePathSegment(sessionId)) {
1037
+ return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
1038
+ }
1039
+ const jsonlPath = join(homedir(), ".openclaw", "agents", agentId, "sessions", `${sessionId}.jsonl`);
1040
+ try {
1041
+ if (!existsSync(jsonlPath)) {
1042
+ return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
1043
+ }
1044
+ const raw = readFileSync(jsonlPath, "utf8");
1045
+ const lines = raw.split("\n");
1046
+ let tokens = 0;
1047
+ let costUsd = 0;
1048
+ let hadError = false;
1049
+ let errorMessage = null;
1050
+ for (const line of lines) {
1051
+ const trimmed = line.trim();
1052
+ if (!trimmed)
1053
+ continue;
1054
+ try {
1055
+ const evt = JSON.parse(trimmed);
1056
+ if (evt.type !== "message")
1057
+ continue;
1058
+ const msg = evt.message;
1059
+ if (!msg || typeof msg !== "object")
1060
+ continue;
1061
+ const usage = msg.usage;
1062
+ if (usage && typeof usage === "object") {
1063
+ const totalTokens = toFiniteNumber(usage.totalTokens) ??
1064
+ toFiniteNumber(usage.total_tokens) ??
1065
+ null;
1066
+ const inputTokens = toFiniteNumber(usage.input) ?? 0;
1067
+ const outputTokens = toFiniteNumber(usage.output) ?? 0;
1068
+ const cacheReadTokens = toFiniteNumber(usage.cacheRead) ?? 0;
1069
+ const cacheWriteTokens = toFiniteNumber(usage.cacheWrite) ?? 0;
1070
+ tokens += Math.max(0, Math.round(totalTokens ??
1071
+ inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens));
1072
+ const cost = usage.cost;
1073
+ const costTotal = cost ? toFiniteNumber(cost.total) : null;
1074
+ if (costTotal !== null) {
1075
+ costUsd += Math.max(0, costTotal);
1076
+ }
1077
+ }
1078
+ const stopReason = typeof msg.stopReason === "string" ? msg.stopReason : "";
1079
+ const msgError = typeof msg.errorMessage === "string" && msg.errorMessage.trim().length > 0
1080
+ ? msg.errorMessage.trim()
1081
+ : null;
1082
+ if (stopReason === "error" || msgError) {
1083
+ hadError = true;
1084
+ errorMessage = msgError ?? errorMessage;
1085
+ }
1086
+ }
1087
+ catch {
1088
+ // Ignore malformed lines.
1089
+ }
1090
+ }
1091
+ return {
1092
+ tokens,
1093
+ costUsd: Math.round(costUsd * 10_000) / 10_000,
1094
+ hadError,
1095
+ errorMessage,
1096
+ };
1097
+ }
1098
+ catch {
1099
+ return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
1100
+ }
1101
+ }
1102
+ async function reconcileStoppedAgentRuns() {
1103
+ try {
1104
+ const store = readAgentRuns();
1105
+ const runs = Object.values(store.runs ?? {});
1106
+ for (const run of runs) {
1107
+ if (!run || typeof run !== "object")
1108
+ continue;
1109
+ if (run.status !== "running")
1110
+ continue;
1111
+ if (!run.pid || isPidAlive(run.pid))
1112
+ continue;
1113
+ const stopped = markAgentRunStopped(run.runId);
1114
+ if (!stopped)
1115
+ continue;
1116
+ const initiativeId = stopped.initiativeId?.trim() ?? "";
1117
+ if (!initiativeId)
1118
+ continue;
1119
+ const summary = readOpenClawSessionSummary({
1120
+ agentId: stopped.agentId,
1121
+ sessionId: stopped.runId,
1122
+ });
1123
+ const completedAt = stopped.stoppedAt ?? new Date().toISOString();
1124
+ const success = !summary.hadError;
1125
+ const correlationId = stopped.runId;
1126
+ const outcomePayload = {
1127
+ initiative_id: initiativeId,
1128
+ correlation_id: correlationId,
1129
+ source_client: "openclaw",
1130
+ execution_id: `openclaw:${stopped.runId}`,
1131
+ execution_type: "openclaw.session",
1132
+ agent_id: stopped.agentId,
1133
+ task_type: stopped.taskId ?? undefined,
1134
+ started_at: stopped.startedAt,
1135
+ completed_at: completedAt,
1136
+ inputs: {
1137
+ message: stopped.message,
1138
+ workstream_id: stopped.workstreamId,
1139
+ task_id: stopped.taskId,
1140
+ },
1141
+ outputs: {
1142
+ had_error: summary.hadError,
1143
+ error_message: summary.errorMessage,
1144
+ },
1145
+ steps: [],
1146
+ success,
1147
+ human_interventions: 0,
1148
+ errors: summary.errorMessage ? [summary.errorMessage] : [],
1149
+ metadata: {
1150
+ provider: stopped.provider,
1151
+ model: stopped.model,
1152
+ tokens: summary.tokens,
1153
+ cost_usd: summary.costUsd,
1154
+ source: "openclaw_agent_run_reconcile",
1155
+ },
1156
+ };
1157
+ const retroEntityType = stopped.taskId
1158
+ ? "task"
1159
+ : "initiative";
1160
+ const retroEntityId = stopped.taskId ?? initiativeId;
1161
+ const retroSummary = stopped.taskId
1162
+ ? `OpenClaw ${success ? "completed" : "blocked"} task ${stopped.taskId}.`
1163
+ : `OpenClaw run ${success ? "completed" : "blocked"} (session ${stopped.runId}).`;
1164
+ const retroPayload = {
1165
+ initiative_id: initiativeId,
1166
+ correlation_id: correlationId,
1167
+ source_client: "openclaw",
1168
+ entity_type: retroEntityType,
1169
+ entity_id: retroEntityId,
1170
+ title: stopped.taskId ?? stopped.runId,
1171
+ idempotency_key: `retro:${stopped.runId}`,
1172
+ retro: {
1173
+ summary: retroSummary,
1174
+ what_went_well: success ? ["Completed without runtime error."] : [],
1175
+ what_went_wrong: success
1176
+ ? []
1177
+ : [summary.errorMessage ?? "Session ended with error."],
1178
+ decisions: [],
1179
+ follow_ups: success
1180
+ ? []
1181
+ : [
1182
+ {
1183
+ title: "Investigate OpenClaw session failure and unblock task",
1184
+ priority: "p0",
1185
+ reason: summary.errorMessage ?? "Session ended with error.",
1186
+ },
1187
+ ],
1188
+ signals: {
1189
+ tokens: summary.tokens,
1190
+ cost_usd: summary.costUsd,
1191
+ had_error: summary.hadError,
1192
+ error_message: summary.errorMessage,
1193
+ session_id: stopped.runId,
1194
+ task_id: stopped.taskId,
1195
+ workstream_id: stopped.workstreamId,
1196
+ provider: stopped.provider,
1197
+ model: stopped.model,
1198
+ source: "openclaw_agent_run_reconcile",
1199
+ },
1200
+ },
1201
+ };
1202
+ try {
1203
+ await client.recordRunOutcome(outcomePayload);
1204
+ }
1205
+ catch (err) {
1206
+ const timestamp = new Date().toISOString();
1207
+ const activityItem = {
1208
+ id: randomUUID(),
1209
+ type: "run_completed",
1210
+ title: `Buffered outcome for session ${stopped.runId}`,
1211
+ description: null,
1212
+ agentId: stopped.agentId,
1213
+ agentName: null,
1214
+ runId: stopped.runId,
1215
+ initiativeId,
1216
+ timestamp,
1217
+ phase: success ? "completed" : "blocked",
1218
+ summary: retroSummary,
1219
+ metadata: {
1220
+ source: "openclaw_local_fallback",
1221
+ error: toErrorMessage(err),
1222
+ },
1223
+ };
1224
+ await appendToOutbox(initiativeId, {
1225
+ id: randomUUID(),
1226
+ type: "outcome",
1227
+ timestamp,
1228
+ payload: outcomePayload,
1229
+ activityItem,
1230
+ });
1231
+ }
1232
+ try {
1233
+ await client.recordRunRetro(retroPayload);
1234
+ }
1235
+ catch (err) {
1236
+ const timestamp = new Date().toISOString();
1237
+ const activityItem = {
1238
+ id: randomUUID(),
1239
+ type: "artifact_created",
1240
+ title: `Buffered retro for session ${stopped.runId}`,
1241
+ description: null,
1242
+ agentId: stopped.agentId,
1243
+ agentName: null,
1244
+ runId: stopped.runId,
1245
+ initiativeId,
1246
+ timestamp,
1247
+ phase: success ? "completed" : "blocked",
1248
+ summary: retroSummary,
1249
+ metadata: {
1250
+ source: "openclaw_local_fallback",
1251
+ error: toErrorMessage(err),
1252
+ },
1253
+ };
1254
+ await appendToOutbox(initiativeId, {
1255
+ id: randomUUID(),
1256
+ type: "retro",
1257
+ timestamp,
1258
+ payload: retroPayload,
1259
+ activityItem,
1260
+ });
1261
+ }
1262
+ }
1263
+ }
1264
+ catch {
1265
+ // best effort
1266
+ }
1267
+ }
819
1268
  async function replayOutboxEvent(event) {
820
1269
  const payload = event.payload ?? {};
1270
+ function normalizeRunFields(context) {
1271
+ // We prefer correlation IDs for replay because many local adapters use UUID-like
1272
+ // session IDs that do *not* exist as server-side run IDs.
1273
+ if (context.correlationId) {
1274
+ return { run_id: undefined, correlation_id: context.correlationId };
1275
+ }
1276
+ if (context.runId) {
1277
+ return {
1278
+ run_id: undefined,
1279
+ correlation_id: `openclaw_run_${stableHash(context.runId).slice(0, 24)}`,
1280
+ };
1281
+ }
1282
+ return { run_id: undefined, correlation_id: undefined };
1283
+ }
821
1284
  if (event.type === "progress") {
822
1285
  const message = extractProgressOutboxMessage(payload);
823
1286
  if (!message) {
@@ -848,7 +1311,12 @@ export default function register(api) {
848
1311
  const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
849
1312
  ? metaRaw
850
1313
  : {};
851
- const emitPayload = {
1314
+ const baseMetadata = {
1315
+ ...meta,
1316
+ source: "orgx_openclaw_outbox_replay",
1317
+ outbox_event_id: event.id,
1318
+ };
1319
+ let emitPayload = {
852
1320
  initiative_id: context.value.initiativeId,
853
1321
  run_id: context.value.runId,
854
1322
  correlation_id: context.value.correlationId,
@@ -860,12 +1328,22 @@ export default function register(api) {
860
1328
  next_step: pickStringField(payload, "next_step") ??
861
1329
  pickStringField(payload, "nextStep") ??
862
1330
  undefined,
863
- metadata: {
864
- ...meta,
865
- source: "orgx_openclaw_outbox_replay",
866
- outbox_event_id: event.id,
867
- },
1331
+ metadata: baseMetadata,
868
1332
  };
1333
+ // Locally-buffered progress events often store a local UUID in run_id. OrgX may reject
1334
+ // unknown run IDs on replay; prefer a deterministic non-UUID correlation key instead.
1335
+ if (emitPayload.run_id && !emitPayload.correlation_id) {
1336
+ const replayCorrelationId = `openclaw_run_${stableHash(emitPayload.run_id).slice(0, 24)}`;
1337
+ emitPayload = {
1338
+ ...emitPayload,
1339
+ run_id: undefined,
1340
+ correlation_id: replayCorrelationId,
1341
+ metadata: {
1342
+ ...(emitPayload.metadata ?? {}),
1343
+ replay_run_id_as_correlation: true,
1344
+ },
1345
+ };
1346
+ }
869
1347
  try {
870
1348
  await client.emitActivity(emitPayload);
871
1349
  }
@@ -879,14 +1357,13 @@ export default function register(api) {
879
1357
  /^404\\b/.test(msg) &&
880
1358
  /\\brun\\b/i.test(msg) &&
881
1359
  /not found/i.test(msg)) {
1360
+ const replayCorrelationId = `openclaw_run_${stableHash(emitPayload.run_id).slice(0, 24)}`;
882
1361
  await client.emitActivity({
883
1362
  ...emitPayload,
884
1363
  run_id: undefined,
885
- // Avoid passing a bare UUID as correlation_id since some server
886
- // paths may interpret UUIDs as run_id lookups.
887
- correlation_id: `openclaw:${emitPayload.run_id}`,
1364
+ correlation_id: replayCorrelationId,
888
1365
  metadata: {
889
- ...emitPayload.metadata,
1366
+ ...(emitPayload.metadata ?? {}),
890
1367
  replay_run_id_as_correlation: true,
891
1368
  },
892
1369
  });
@@ -909,12 +1386,29 @@ export default function register(api) {
909
1386
  if (!context.ok) {
910
1387
  throw new Error(context.error);
911
1388
  }
1389
+ const runFields = normalizeRunFields({
1390
+ runId: context.value.runId,
1391
+ correlationId: context.value.correlationId,
1392
+ });
1393
+ // Payloads should include a stable idempotency_key when enqueued, but older
1394
+ // events may not. Derive a deterministic fallback so outbox replay won't
1395
+ // double-create the same remote decision.
1396
+ const fallbackKey = stableHash(JSON.stringify({
1397
+ t: "decision",
1398
+ initiative_id: context.value.initiativeId,
1399
+ run_id: context.value.runId ?? null,
1400
+ correlation_id: context.value.correlationId ?? null,
1401
+ question,
1402
+ })).slice(0, 24);
1403
+ const resolvedIdempotencyKey = pickStringField(payload, "idempotency_key") ??
1404
+ pickStringField(payload, "idempotencyKey") ??
1405
+ `openclaw:decision:${fallbackKey}`;
912
1406
  await client.applyChangeset({
913
1407
  initiative_id: context.value.initiativeId,
914
- run_id: context.value.runId,
915
- correlation_id: context.value.correlationId,
1408
+ run_id: runFields.run_id,
1409
+ correlation_id: runFields.correlation_id,
916
1410
  source_client: context.value.sourceClient,
917
- idempotency_key: pickStringField(payload, "idempotency_key") ?? `decision:${event.id}`,
1411
+ idempotency_key: resolvedIdempotencyKey,
918
1412
  operations: [
919
1413
  {
920
1414
  op: "decision.create",
@@ -933,6 +1427,10 @@ export default function register(api) {
933
1427
  if (!context.ok) {
934
1428
  throw new Error(context.error);
935
1429
  }
1430
+ const runFields = normalizeRunFields({
1431
+ runId: context.value.runId,
1432
+ correlationId: context.value.correlationId,
1433
+ });
936
1434
  const operations = Array.isArray(payload.operations)
937
1435
  ? payload.operations
938
1436
  : [];
@@ -942,33 +1440,230 @@ export default function register(api) {
942
1440
  });
943
1441
  return;
944
1442
  }
1443
+ // Status updates are the most common offline replay payload, and `updateEntity`
1444
+ // is the most widely supported primitive across OrgX deployments. Prefer it
1445
+ // when the changeset contains only simple status mutations.
1446
+ const statusOps = operations
1447
+ .map((op) => {
1448
+ if (!op || typeof op !== "object")
1449
+ return null;
1450
+ const record = op;
1451
+ const kind = typeof record.op === "string" ? record.op.trim() : "";
1452
+ if (kind === "task.update") {
1453
+ const taskId = typeof record.task_id === "string" ? record.task_id.trim() : "";
1454
+ const statusRaw = typeof record.status === "string" ? record.status.trim() : "";
1455
+ const normalized = statusRaw.toLowerCase().replace(/\s+/g, "_");
1456
+ const status = normalized === "completed" || normalized === "complete" || normalized === "finished"
1457
+ ? "done"
1458
+ : normalized === "inprogress"
1459
+ ? "in_progress"
1460
+ : normalized;
1461
+ if (!taskId || !status)
1462
+ return null;
1463
+ return { type: "task", id: taskId, status };
1464
+ }
1465
+ if (kind === "milestone.update") {
1466
+ const milestoneId = typeof record.milestone_id === "string" ? record.milestone_id.trim() : "";
1467
+ const statusRaw = typeof record.status === "string" ? record.status.trim() : "";
1468
+ const normalized = statusRaw.toLowerCase().replace(/\s+/g, "_");
1469
+ const status = normalized === "done" || normalized === "complete" || normalized === "finished"
1470
+ ? "completed"
1471
+ : normalized === "inprogress"
1472
+ ? "in_progress"
1473
+ : normalized === "todo" || normalized === "not_started" || normalized === "pending"
1474
+ ? "planned"
1475
+ : normalized === "blocked" || normalized === "stuck"
1476
+ ? "at_risk"
1477
+ : normalized;
1478
+ if (!milestoneId || !status)
1479
+ return null;
1480
+ return { type: "milestone", id: milestoneId, status };
1481
+ }
1482
+ return null;
1483
+ })
1484
+ .filter((item) => Boolean(item));
1485
+ if (statusOps.length === operations.length) {
1486
+ for (const op of statusOps) {
1487
+ await client.updateEntity(op.type, op.id, { status: op.status });
1488
+ }
1489
+ return;
1490
+ }
1491
+ // Payloads should include a stable idempotency_key when enqueued, but older
1492
+ // events may not. Derive a deterministic fallback so outbox replay won't
1493
+ // double-create the same remote change.
1494
+ const fallbackKey = stableHash(JSON.stringify({
1495
+ t: "changeset",
1496
+ initiative_id: context.value.initiativeId,
1497
+ run_id: context.value.runId ?? null,
1498
+ correlation_id: context.value.correlationId ?? null,
1499
+ operations,
1500
+ })).slice(0, 24);
1501
+ const resolvedIdempotencyKey = pickStringField(payload, "idempotency_key") ??
1502
+ pickStringField(payload, "idempotencyKey") ??
1503
+ `openclaw:changeset:${fallbackKey}`;
945
1504
  await client.applyChangeset({
946
1505
  initiative_id: context.value.initiativeId,
947
- run_id: context.value.runId,
948
- correlation_id: context.value.correlationId,
1506
+ run_id: runFields.run_id,
1507
+ correlation_id: runFields.correlation_id,
949
1508
  source_client: context.value.sourceClient,
950
- idempotency_key: pickStringField(payload, "idempotency_key") ?? `changeset:${event.id}`,
1509
+ idempotency_key: resolvedIdempotencyKey,
951
1510
  operations,
952
1511
  });
953
1512
  return;
954
1513
  }
955
- if (event.type === "artifact") {
956
- const name = pickStringField(payload, "name");
957
- if (!name) {
958
- api.log?.warn?.("[orgx] Dropping invalid artifact outbox event", {
1514
+ if (event.type === "outcome") {
1515
+ const context = resolveReportingContext(payload);
1516
+ if (!context.ok) {
1517
+ throw new Error(context.error);
1518
+ }
1519
+ const runFields = normalizeRunFields({
1520
+ runId: context.value.runId,
1521
+ correlationId: context.value.correlationId,
1522
+ });
1523
+ const executionId = pickStringField(payload, "execution_id") ??
1524
+ pickStringField(payload, "executionId");
1525
+ const executionType = pickStringField(payload, "execution_type") ??
1526
+ pickStringField(payload, "executionType");
1527
+ const agentId = pickStringField(payload, "agent_id") ??
1528
+ pickStringField(payload, "agentId");
1529
+ const success = typeof payload.success === "boolean"
1530
+ ? payload.success
1531
+ : null;
1532
+ if (!executionId || !executionType || !agentId || success === null) {
1533
+ api.log?.warn?.("[orgx] Dropping invalid outcome outbox event", {
1534
+ eventId: event.id,
1535
+ });
1536
+ return;
1537
+ }
1538
+ const metaRaw = payload.metadata;
1539
+ const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
1540
+ ? metaRaw
1541
+ : {};
1542
+ await client.recordRunOutcome({
1543
+ initiative_id: context.value.initiativeId,
1544
+ run_id: runFields.run_id,
1545
+ correlation_id: runFields.correlation_id,
1546
+ source_client: context.value.sourceClient,
1547
+ execution_id: executionId,
1548
+ execution_type: executionType,
1549
+ agent_id: agentId,
1550
+ task_type: pickStringField(payload, "task_type") ??
1551
+ pickStringField(payload, "taskType") ??
1552
+ undefined,
1553
+ domain: pickStringField(payload, "domain") ?? undefined,
1554
+ started_at: pickStringField(payload, "started_at") ??
1555
+ pickStringField(payload, "startedAt") ??
1556
+ undefined,
1557
+ completed_at: pickStringField(payload, "completed_at") ??
1558
+ pickStringField(payload, "completedAt") ??
1559
+ undefined,
1560
+ inputs: payload.inputs && typeof payload.inputs === "object"
1561
+ ? payload.inputs
1562
+ : undefined,
1563
+ outputs: payload.outputs && typeof payload.outputs === "object"
1564
+ ? payload.outputs
1565
+ : undefined,
1566
+ steps: Array.isArray(payload.steps)
1567
+ ? payload.steps
1568
+ : undefined,
1569
+ success,
1570
+ quality_score: typeof payload.quality_score === "number"
1571
+ ? payload.quality_score
1572
+ : typeof payload.qualityScore === "number"
1573
+ ? payload.qualityScore
1574
+ : undefined,
1575
+ duration_vs_estimate: typeof payload.duration_vs_estimate === "number"
1576
+ ? payload.duration_vs_estimate
1577
+ : typeof payload.durationVsEstimate === "number"
1578
+ ? payload.durationVsEstimate
1579
+ : undefined,
1580
+ cost_vs_budget: typeof payload.cost_vs_budget === "number"
1581
+ ? payload.cost_vs_budget
1582
+ : typeof payload.costVsBudget === "number"
1583
+ ? payload.costVsBudget
1584
+ : undefined,
1585
+ human_interventions: typeof payload.human_interventions === "number"
1586
+ ? payload.human_interventions
1587
+ : typeof payload.humanInterventions === "number"
1588
+ ? payload.humanInterventions
1589
+ : undefined,
1590
+ user_satisfaction: typeof payload.user_satisfaction === "number"
1591
+ ? payload.user_satisfaction
1592
+ : typeof payload.userSatisfaction === "number"
1593
+ ? payload.userSatisfaction
1594
+ : undefined,
1595
+ errors: Array.isArray(payload.errors)
1596
+ ? payload.errors.filter((e) => typeof e === "string")
1597
+ : undefined,
1598
+ metadata: {
1599
+ ...meta,
1600
+ source: "orgx_openclaw_outbox_replay",
1601
+ outbox_event_id: event.id,
1602
+ },
1603
+ });
1604
+ return;
1605
+ }
1606
+ if (event.type === "retro") {
1607
+ const context = resolveReportingContext(payload);
1608
+ if (!context.ok) {
1609
+ throw new Error(context.error);
1610
+ }
1611
+ const runFields = normalizeRunFields({
1612
+ runId: context.value.runId,
1613
+ correlationId: context.value.correlationId,
1614
+ });
1615
+ const retro = payload.retro && typeof payload.retro === "object" && !Array.isArray(payload.retro)
1616
+ ? payload.retro
1617
+ : null;
1618
+ const summary = retro && typeof retro.summary === "string" ? retro.summary.trim() : "";
1619
+ if (!retro || !summary) {
1620
+ api.log?.warn?.("[orgx] Dropping invalid retro outbox event", {
959
1621
  eventId: event.id,
960
1622
  });
961
1623
  return;
962
1624
  }
963
- await client.createEntity("artifact", {
964
- name,
965
- artifact_type: pickStringField(payload, "artifact_type") ?? "other",
966
- description: pickStringField(payload, "description"),
967
- artifact_url: pickStringField(payload, "url"),
968
- status: "active",
1625
+ const entityTypeRaw = pickStringField(payload, "entity_type") ??
1626
+ pickStringField(payload, "entityType");
1627
+ const parsedEntityType = parseRetroEntityType(entityTypeRaw) ?? null;
1628
+ // Server-side enum parity can lag behind local clients. Only attach to the
1629
+ // entity types that are guaranteed to exist today.
1630
+ const entityType = parsedEntityType === "initiative" || parsedEntityType === "task"
1631
+ ? parsedEntityType
1632
+ : null;
1633
+ const entityIdRaw = pickStringField(payload, "entity_id") ??
1634
+ pickStringField(payload, "entityId") ??
1635
+ null;
1636
+ const entityId = isUuid(entityIdRaw ?? undefined) ? entityIdRaw : null;
1637
+ await client.recordRunRetro({
1638
+ initiative_id: context.value.initiativeId,
1639
+ run_id: runFields.run_id,
1640
+ correlation_id: runFields.correlation_id,
1641
+ source_client: context.value.sourceClient,
1642
+ entity_type: entityType && entityId ? entityType : undefined,
1643
+ entity_id: entityType && entityId ? entityId : undefined,
1644
+ title: pickStringField(payload, "title") ?? undefined,
1645
+ idempotency_key: pickStringField(payload, "idempotency_key") ??
1646
+ pickStringField(payload, "idempotencyKey") ??
1647
+ undefined,
1648
+ retro: retro,
1649
+ markdown: pickStringField(payload, "markdown") ?? undefined,
969
1650
  });
970
1651
  return;
971
1652
  }
1653
+ if (event.type === "artifact") {
1654
+ // Artifacts are UI-level breadcrumbs and may not be supported by every
1655
+ // OrgX deployment's `/api/entities` schema. Persist locally and drop from
1656
+ // the outbox so progress reporting doesn't wedge on irreplayable items.
1657
+ try {
1658
+ if (event.activityItem) {
1659
+ appendActivityItems([event.activityItem]);
1660
+ }
1661
+ }
1662
+ catch {
1663
+ // best effort
1664
+ }
1665
+ return;
1666
+ }
972
1667
  }
973
1668
  async function flushOutboxQueues() {
974
1669
  const attemptAt = new Date().toISOString();
@@ -1040,6 +1735,9 @@ export default function register(api) {
1040
1735
  return syncInFlight;
1041
1736
  }
1042
1737
  syncInFlight = (async () => {
1738
+ if (!config.apiKey) {
1739
+ refreshConfigFromSources({ reason: "sync_no_api_key" });
1740
+ }
1043
1741
  if (!config.apiKey) {
1044
1742
  updateOnboardingState({
1045
1743
  status: "idle",
@@ -1050,25 +1748,123 @@ export default function register(api) {
1050
1748
  return;
1051
1749
  }
1052
1750
  try {
1053
- updateCachedSnapshot(await client.getOrgSnapshot());
1751
+ await reconcileStoppedAgentRuns();
1752
+ let snapshotError = null;
1753
+ try {
1754
+ updateCachedSnapshot(await client.getOrgSnapshot());
1755
+ }
1756
+ catch (err) {
1757
+ if (isAuthFailure(err)) {
1758
+ throw err;
1759
+ }
1760
+ snapshotError = toErrorMessage(err);
1761
+ api.log?.warn?.("[orgx] Snapshot sync failed (continuing)", {
1762
+ error: snapshotError,
1763
+ });
1764
+ }
1765
+ // Best-effort: poll the canonical OrgX SkillPack so the dashboard/install path
1766
+ // can apply it without blocking on an on-demand fetch.
1767
+ try {
1768
+ const refreshed = await refreshSkillPackState({
1769
+ getSkillPack: (args) => client.getSkillPack(args),
1770
+ });
1771
+ if (refreshed.changed) {
1772
+ void posthogCapture({
1773
+ event: "openclaw_skill_pack_updated",
1774
+ distinctId: config.installationId,
1775
+ properties: {
1776
+ plugin_version: config.pluginVersion,
1777
+ skill_pack_name: refreshed.state.pack?.name ?? null,
1778
+ skill_pack_version: refreshed.state.pack?.version ?? null,
1779
+ skill_pack_checksum: refreshed.state.pack?.checksum ?? null,
1780
+ },
1781
+ }).catch(() => {
1782
+ // best effort
1783
+ });
1784
+ }
1785
+ }
1786
+ catch {
1787
+ // best effort
1788
+ }
1789
+ // Best-effort: provision/update the OrgX agent suite after we've verified a working connection.
1790
+ // This makes domain agents available immediately for launches without requiring a manual install.
1791
+ try {
1792
+ if (config.autoInstallAgentSuiteOnConnect !== false) {
1793
+ const state = readSkillPackState();
1794
+ const updateAvailable = Boolean(state.remote?.checksum &&
1795
+ state.pack?.checksum &&
1796
+ state.remote.checksum !== state.pack.checksum);
1797
+ const plan = computeOrgxAgentSuitePlan({
1798
+ packVersion: config.pluginVersion || "0.0.0",
1799
+ skillPack: state.overrides,
1800
+ skillPackRemote: state.remote,
1801
+ skillPackPolicy: state.policy,
1802
+ skillPackUpdateAvailable: updateAvailable,
1803
+ });
1804
+ const hasConflicts = (plan.workspaceFiles ?? []).some((f) => f.action === "conflict");
1805
+ const hasWork = Boolean(plan.openclawConfigWouldUpdate) ||
1806
+ (plan.workspaceFiles ?? []).some((f) => f.action !== "noop");
1807
+ if (hasWork && !hasConflicts) {
1808
+ const applied = applyOrgxAgentSuitePlan({
1809
+ plan,
1810
+ dryRun: false,
1811
+ skillPack: state.overrides,
1812
+ });
1813
+ void applied;
1814
+ void posthogCapture({
1815
+ event: "openclaw_agent_suite_auto_install",
1816
+ distinctId: config.installationId,
1817
+ properties: {
1818
+ plugin_version: (config.pluginVersion ?? "").trim() || null,
1819
+ skill_pack_source: plan.skillPack?.source ?? null,
1820
+ skill_pack_checksum: plan.skillPack?.checksum ?? null,
1821
+ skill_pack_version: plan.skillPack?.version ?? null,
1822
+ openclaw_config_updated: Boolean(plan.openclawConfigWouldUpdate),
1823
+ },
1824
+ }).catch(() => {
1825
+ // best effort
1826
+ });
1827
+ }
1828
+ }
1829
+ }
1830
+ catch (err) {
1831
+ api.log?.debug?.("[orgx] Agent suite auto-provision skipped/failed (best effort)", {
1832
+ error: err instanceof Error ? err.message : String(err),
1833
+ });
1834
+ }
1054
1835
  updateOnboardingState({
1055
1836
  status: "connected",
1056
1837
  hasApiKey: true,
1057
- connectionVerified: true,
1058
- lastError: null,
1838
+ connectionVerified: snapshotError === null,
1839
+ lastError: snapshotError,
1059
1840
  nextAction: "open_dashboard",
1060
1841
  });
1061
1842
  await flushOutboxQueues();
1062
1843
  api.log?.debug?.("[orgx] Sync OK");
1063
1844
  }
1064
1845
  catch (err) {
1846
+ const authFailure = isAuthFailure(err);
1847
+ const errorMessage = authFailure
1848
+ ? "Unauthorized. Your OrgX key may be revoked or expired. Reconnect in browser or use API key."
1849
+ : toErrorMessage(err);
1065
1850
  updateOnboardingState({
1066
1851
  status: "error",
1067
1852
  hasApiKey: true,
1068
1853
  connectionVerified: false,
1069
- lastError: toErrorMessage(err),
1854
+ lastError: errorMessage,
1070
1855
  nextAction: "reconnect",
1071
1856
  });
1857
+ if (authFailure) {
1858
+ void posthogCapture({
1859
+ event: "openclaw_sync_auth_failed",
1860
+ distinctId: config.installationId,
1861
+ properties: {
1862
+ plugin_version: config.pluginVersion,
1863
+ },
1864
+ }).catch(() => {
1865
+ // best effort
1866
+ });
1867
+ }
1072
1868
  api.log?.warn?.(`[orgx] Sync failed: ${err instanceof Error ? err.message : err}`);
1073
1869
  }
1074
1870
  })();
@@ -1100,7 +1896,10 @@ export default function register(api) {
1100
1896
  openclawVersion: input.openclawVersion,
1101
1897
  platform: input.platform || process.platform,
1102
1898
  deviceName: input.deviceName,
1103
- });
1899
+ },
1900
+ // Pairing can hit a cold serverless boot + supabase insert + rate-limit checks.
1901
+ // Give it more headroom than typical lightweight API calls.
1902
+ { timeoutMs: 30_000 });
1104
1903
  if (!started.ok) {
1105
1904
  if (isAuthRequiredError(started)) {
1106
1905
  clearPairingState();
@@ -1124,7 +1923,8 @@ export default function register(api) {
1124
1923
  state,
1125
1924
  };
1126
1925
  }
1127
- const message = `Pairing start failed: ${started.error}`;
1926
+ const statusLabel = started.status ? ` (HTTP ${started.status})` : "";
1927
+ const message = `Pairing start failed${statusLabel}: ${started.error}`;
1128
1928
  updateOnboardingState({
1129
1929
  status: "error",
1130
1930
  hasApiKey: Boolean(config.apiKey),
@@ -1875,6 +2675,48 @@ export default function register(api) {
1875
2675
  }
1876
2676
  },
1877
2677
  }, { optional: true });
2678
+ function withProvenanceMetadata(metadata) {
2679
+ const input = metadata ?? {};
2680
+ const out = { ...input };
2681
+ if (out.orgx_plugin_version === undefined) {
2682
+ out.orgx_plugin_version = (config.pluginVersion ?? "").trim() || null;
2683
+ }
2684
+ try {
2685
+ const state = readSkillPackState();
2686
+ const overrides = state.overrides;
2687
+ if (out.skill_pack_name === undefined) {
2688
+ out.skill_pack_name = overrides?.name ?? state.pack?.name ?? null;
2689
+ }
2690
+ if (out.skill_pack_version === undefined) {
2691
+ out.skill_pack_version = overrides?.version ?? state.pack?.version ?? null;
2692
+ }
2693
+ if (out.skill_pack_checksum === undefined) {
2694
+ out.skill_pack_checksum = overrides?.checksum ?? state.pack?.checksum ?? null;
2695
+ }
2696
+ if (out.skill_pack_source === undefined) {
2697
+ out.skill_pack_source = overrides?.source ?? null;
2698
+ }
2699
+ if (out.skill_pack_etag === undefined) {
2700
+ out.skill_pack_etag = state.etag ?? null;
2701
+ }
2702
+ }
2703
+ catch {
2704
+ // best effort
2705
+ }
2706
+ if (out.orgx_provenance === undefined) {
2707
+ out.orgx_provenance = {
2708
+ plugin_version: out.orgx_plugin_version ?? null,
2709
+ skill_pack: {
2710
+ name: out.skill_pack_name ?? null,
2711
+ version: out.skill_pack_version ?? null,
2712
+ checksum: out.skill_pack_checksum ?? null,
2713
+ source: out.skill_pack_source ?? null,
2714
+ etag: out.skill_pack_etag ?? null,
2715
+ },
2716
+ };
2717
+ }
2718
+ return out;
2719
+ }
1878
2720
  async function emitActivityWithFallback(source, payload) {
1879
2721
  if (!payload.message || payload.message.trim().length === 0) {
1880
2722
  return text("❌ message is required");
@@ -1895,10 +2737,10 @@ export default function register(api) {
1895
2737
  progress_pct: payload.progress_pct,
1896
2738
  level: payload.level ?? "info",
1897
2739
  next_step: payload.next_step,
1898
- metadata: {
2740
+ metadata: withProvenanceMetadata({
1899
2741
  ...(payload.metadata ?? {}),
1900
2742
  source,
1901
- },
2743
+ }),
1902
2744
  };
1903
2745
  const activityItem = {
1904
2746
  id,
@@ -1961,10 +2803,10 @@ export default function register(api) {
1961
2803
  timestamp: now,
1962
2804
  phase: "review",
1963
2805
  summary: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
1964
- metadata: {
2806
+ metadata: withProvenanceMetadata({
1965
2807
  source,
1966
2808
  idempotency_key: idempotencyKey,
1967
- },
2809
+ }),
1968
2810
  };
1969
2811
  try {
1970
2812
  const result = await client.applyChangeset(requestPayload);
@@ -2239,6 +3081,10 @@ export default function register(api) {
2239
3081
  parameters: {
2240
3082
  type: "object",
2241
3083
  properties: {
3084
+ initiative_id: {
3085
+ type: "string",
3086
+ description: "Optional initiative UUID to attach this artifact to",
3087
+ },
2242
3088
  name: {
2243
3089
  type: "string",
2244
3090
  description: "Human-readable artifact name (e.g., 'PR #107: Fix build size')",
@@ -2263,6 +3109,9 @@ export default function register(api) {
2263
3109
  async execute(_callId, params = { name: "", artifact_type: "other" }) {
2264
3110
  const now = new Date().toISOString();
2265
3111
  const id = `artifact:${randomUUID().slice(0, 8)}`;
3112
+ const initiativeId = isUuid(params.initiative_id)
3113
+ ? params.initiative_id
3114
+ : inferReportingInitiativeId(params) ?? null;
2266
3115
  const activityItem = {
2267
3116
  id,
2268
3117
  type: "artifact_created",
@@ -2271,20 +3120,21 @@ export default function register(api) {
2271
3120
  agentId: null,
2272
3121
  agentName: null,
2273
3122
  runId: null,
2274
- initiativeId: null,
3123
+ initiativeId,
2275
3124
  timestamp: now,
2276
3125
  summary: params.url ?? null,
2277
- metadata: {
3126
+ metadata: withProvenanceMetadata({
2278
3127
  source: "orgx_register_artifact",
2279
3128
  artifact_type: params.artifact_type,
2280
3129
  url: params.url,
2281
- },
3130
+ }),
2282
3131
  };
2283
3132
  try {
2284
3133
  const entity = await client.createEntity("artifact", {
2285
- name: params.name,
3134
+ title: params.name,
2286
3135
  artifact_type: params.artifact_type,
2287
- description: params.description,
3136
+ summary: params.description,
3137
+ initiative_id: initiativeId ?? undefined,
2288
3138
  artifact_url: params.url,
2289
3139
  status: "active",
2290
3140
  });
@@ -2295,7 +3145,10 @@ export default function register(api) {
2295
3145
  id,
2296
3146
  type: "artifact",
2297
3147
  timestamp: now,
2298
- payload: params,
3148
+ payload: {
3149
+ ...params,
3150
+ initiative_id: initiativeId,
3151
+ },
2299
3152
  activityItem,
2300
3153
  });
2301
3154
  return text(`Artifact saved locally: ${params.name} [${params.artifact_type}] (will sync when connected)`);
@@ -2406,8 +3259,29 @@ export default function register(api) {
2406
3259
  }, {
2407
3260
  getHealth: async (input = {}) => buildHealthReport({ probeRemote: input.probeRemote === true }),
2408
3261
  });
3262
+ const mcpPromptRegistry = new Map();
3263
+ mcpPromptRegistry.set("ship", {
3264
+ name: "ship",
3265
+ description: "Commit local changes, open a PR, and merge it (GitHub CLI required).",
3266
+ arguments: [],
3267
+ messages: [
3268
+ {
3269
+ role: "user",
3270
+ content: [
3271
+ "Ship the current work:",
3272
+ "- Inspect `git status -sb` and `git diff --stat` and summarize what will be shipped.",
3273
+ "- Run `npm run typecheck`, `npm run test:hooks`, and `npm run build` (fix failures).",
3274
+ "- Create a feature branch if on `main`.",
3275
+ "- Commit with a clear message (do not include secrets).",
3276
+ "- Push branch, open a PR (use `gh pr create`), then merge it (use `gh pr merge --merge --auto`).",
3277
+ "- If `gh` is not authenticated, stop and tell me what to run.",
3278
+ ].join("\n"),
3279
+ },
3280
+ ],
3281
+ });
2409
3282
  const mcpHttpHandler = createMcpHttpHandler({
2410
3283
  tools: mcpToolRegistry,
3284
+ prompts: mcpPromptRegistry,
2411
3285
  logger: api.log ?? {},
2412
3286
  serverName: "@useorgx/openclaw-plugin",
2413
3287
  serverVersion: config.pluginVersion,