@sireai/optimus 0.1.14 → 0.1.16

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 (74) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/feedback-delivery.d.ts +56 -0
  3. package/dist/cli/feedback-delivery.js +297 -0
  4. package/dist/cli/feedback-delivery.js.map +1 -0
  5. package/dist/cli/feedback.d.ts +25 -0
  6. package/dist/cli/feedback.js +143 -0
  7. package/dist/cli/feedback.js.map +1 -0
  8. package/dist/cli/optimus.js +397 -199
  9. package/dist/cli/optimus.js.map +1 -1
  10. package/dist/config/load-config.js +5 -4
  11. package/dist/config/load-config.js.map +1 -1
  12. package/dist/config/optimus-paths.d.ts +1 -0
  13. package/dist/config/optimus-paths.js +3 -0
  14. package/dist/config/optimus-paths.js.map +1 -1
  15. package/dist/integrations/feishu/feishu-auth-config.d.ts +5 -1
  16. package/dist/integrations/feishu/feishu-auth-config.js +9 -6
  17. package/dist/integrations/feishu/feishu-auth-config.js.map +1 -1
  18. package/dist/integrations/feishu/feishu-client.d.ts +2 -6
  19. package/dist/integrations/feishu/feishu-client.js +16 -41
  20. package/dist/integrations/feishu/feishu-client.js.map +1 -1
  21. package/dist/integrations/feishu/feishu-doc-service.d.ts +8 -3
  22. package/dist/integrations/feishu/feishu-doc-service.js +16 -2
  23. package/dist/integrations/feishu/feishu-doc-service.js.map +1 -1
  24. package/dist/integrations/feishu/feishu-user-service.d.ts +1 -2
  25. package/dist/integrations/feishu/feishu-user-service.js +1 -2
  26. package/dist/integrations/feishu/feishu-user-service.js.map +1 -1
  27. package/dist/integrations/jira/jira-cli.js +21 -2
  28. package/dist/integrations/jira/jira-cli.js.map +1 -1
  29. package/dist/integrations/jira/jira-client.d.ts +2 -1
  30. package/dist/integrations/jira/jira-client.js +19 -2
  31. package/dist/integrations/jira/jira-client.js.map +1 -1
  32. package/dist/integrations/jira/jira-submit.js +24 -24
  33. package/dist/integrations/jira/jira-submit.js.map +1 -1
  34. package/dist/integrations/sentry/sentry-cli.js +21 -1
  35. package/dist/integrations/sentry/sentry-cli.js.map +1 -1
  36. package/dist/task-environment/cancellation/task-cleanup-service.d.ts +1 -0
  37. package/dist/task-environment/cancellation/task-cleanup-service.js +22 -1
  38. package/dist/task-environment/cancellation/task-cleanup-service.js.map +1 -1
  39. package/dist/task-environment/delivery/feishu-analysis-doc-service.d.ts +7 -3
  40. package/dist/task-environment/delivery/feishu-analysis-doc-service.js +62 -3
  41. package/dist/task-environment/delivery/feishu-analysis-doc-service.js.map +1 -1
  42. package/dist/task-environment/delivery/feishu-card-renderer.d.ts +1 -0
  43. package/dist/task-environment/delivery/feishu-card-renderer.js +17 -6
  44. package/dist/task-environment/delivery/feishu-card-renderer.js.map +1 -1
  45. package/dist/task-environment/delivery/feishu-notifier.d.ts +13 -0
  46. package/dist/task-environment/delivery/feishu-notifier.js +206 -22
  47. package/dist/task-environment/delivery/feishu-notifier.js.map +1 -1
  48. package/dist/task-environment/delivery/sentry-feishu-card-renderer.d.ts +1 -0
  49. package/dist/task-environment/delivery/sentry-feishu-card-renderer.js +16 -5
  50. package/dist/task-environment/delivery/sentry-feishu-card-renderer.js.map +1 -1
  51. package/dist/task-environment/orchestration/execution-context-assembler.js +5 -3
  52. package/dist/task-environment/orchestration/execution-context-assembler.js.map +1 -1
  53. package/dist/task-environment/orchestration/task-orchestrator.d.ts +1 -1
  54. package/dist/task-environment/orchestration/task-orchestrator.js +40 -24
  55. package/dist/task-environment/orchestration/task-orchestrator.js.map +1 -1
  56. package/dist/task-environment/runtime/optimus-runtime.js +1 -0
  57. package/dist/task-environment/runtime/optimus-runtime.js.map +1 -1
  58. package/dist/types.d.ts +2 -2
  59. package/package.json +3 -5
  60. package/dist/integrations/feishu/feishu-auth-service.d.ts +0 -71
  61. package/dist/integrations/feishu/feishu-auth-service.js +0 -399
  62. package/dist/integrations/feishu/feishu-auth-service.js.map +0 -1
  63. package/dist/integrations/feishu/feishu-auth-store.d.ts +0 -29
  64. package/dist/integrations/feishu/feishu-auth-store.js +0 -113
  65. package/dist/integrations/feishu/feishu-auth-store.js.map +0 -1
  66. package/dist/integrations/jira/jira-cli-config.d.ts +0 -15
  67. package/dist/integrations/jira/jira-cli-config.js +0 -100
  68. package/dist/integrations/jira/jira-cli-config.js.map +0 -1
  69. package/dist/problem-solving-core/codex/codex-global-config.d.ts +0 -17
  70. package/dist/problem-solving-core/codex/codex-global-config.js +0 -100
  71. package/dist/problem-solving-core/codex/codex-global-config.js.map +0 -1
  72. package/dist/task-environment/orchestration/triage-agent.d.ts +0 -54
  73. package/dist/task-environment/orchestration/triage-agent.js +0 -636
  74. package/dist/task-environment/orchestration/triage-agent.js.map +0 -1
@@ -2,7 +2,7 @@
2
2
  import { buildDefaultConfig, loadConfig } from "../config/load-config.js";
3
3
  import { createReadStream, createWriteStream } from "node:fs";
4
4
  import { access, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
5
- import { basename, dirname, join, resolve } from "node:path";
5
+ import { basename, dirname, join, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { execFile, spawn } from "node:child_process";
8
8
  import { createInterface } from "node:readline/promises";
@@ -29,7 +29,10 @@ import { TaskDeliveryDispatcher } from "../task-environment/delivery/task-delive
29
29
  import { TaskDeliveryService } from "../task-environment/delivery/task-delivery-service.js";
30
30
  import { FeishuAnalysisDocService } from "../task-environment/delivery/feishu-analysis-doc-service.js";
31
31
  import { TaskPublicationService } from "../task-environment/delivery/task-publication-service.js";
32
- import { FeishuAuthService } from "../integrations/feishu/feishu-auth-service.js";
32
+ import { FeishuClient } from "../integrations/feishu/feishu-client.js";
33
+ import { FeishuUserService } from "../integrations/feishu/feishu-user-service.js";
34
+ import { createFeedbackReport } from "./feedback.js";
35
+ import { DEFAULT_FEEDBACK_RECIPIENT_EMAIL, FeishuFeedbackDeliveryService, resolveDefaultFeedbackRecipientFallbackOpenId } from "./feedback-delivery.js";
33
36
  import { resolveDefaultConfigPath, resolveDefaultEnvPath, resolveOptimusHomeDir } from "../config/optimus-paths.js";
34
37
  import { checkForSelfUpdate, installSelfUpdate, recordSkippedSelfUpdate, readSelfUpdateState, shouldPromptForCachedStartupUpdate } from "./self-update.js";
35
38
  const CLI_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
@@ -61,6 +64,11 @@ function renderSetupResult(result) {
61
64
  lines.push(`Codex Auth: ${result.codexAuth.mode} | ${result.codexAuth.configured ? `${envLabel} configured` : `${envLabel} missing`}${providerLabel}`);
62
65
  }
63
66
  lines.push("");
67
+ lines.push("Next");
68
+ lines.push("- optimus start");
69
+ lines.push("- optimus submit --title \"Login crash\" --description \"Crash on startup\"");
70
+ lines.push("- optimus doctor --quick");
71
+ lines.push("");
64
72
  lines.push(renderQuickDoctorReport(result.doctorQuick));
65
73
  return lines.join("\n");
66
74
  }
@@ -138,16 +146,41 @@ function renderCliHelp() {
138
146
  "Usage:",
139
147
  " optimus <command> [options]",
140
148
  "",
141
- "Get started:",
142
- " optimus setup",
143
- " optimus start",
144
- " optimus submit --title \"Login crash\" --description \"Crash on startup\"",
149
+ "First time here:",
150
+ " 1. optimus setup",
151
+ " 2. optimus start",
152
+ " 3. optimus submit --title \"Login crash\" --description \"Crash on startup\"",
153
+ "",
154
+ "Everyday commands:",
155
+ " setup First-time setup or update config",
156
+ " start Start the resident runtime",
157
+ " submit Submit a problem for Optimus to handle",
158
+ " task-status Check what is running now",
159
+ " task-result Read the latest result for a task",
160
+ " doctor --quick Quick readiness check",
161
+ " version Show the installed package version",
162
+ "",
163
+ "Need more?",
164
+ " optimus help advanced Show integration, retry, and developer commands",
165
+ " optimus help <command> Show details for one command",
145
166
  "",
146
- "User commands:",
167
+ "Notes:",
168
+ ` - Optimus stores config and runtime data under ${resolveOptimusHomeDir()}.`,
169
+ " - Jira and Sentry are optional integrations. You can enable them later in `optimus setup`."
170
+ ].join("\n");
171
+ }
172
+ function renderAdvancedCliHelp() {
173
+ return [
174
+ "Optimus CLI",
175
+ "",
176
+ "Advanced command index",
177
+ "",
178
+ "Everyday commands:",
147
179
  " help [command] Show CLI help or command help",
148
180
  " setup Configure Optimus for first use or update config",
149
181
  " start Start the resident runtime",
150
182
  " submit Submit a manual task",
183
+ " feedback Create a local user feedback bundle",
151
184
  " retry-task Requeue an existing task",
152
185
  " cancel-task Cancel a queued or running task",
153
186
  " task-status Show task execution status",
@@ -166,7 +199,6 @@ function renderCliHelp() {
166
199
  " postrun-retry Rebuild postrun artifacts and optionally replay publication",
167
200
  "",
168
201
  "Integration commands:",
169
- " feishu auth login|status|logout",
170
202
  " jira-search|jira-get-issue|jira-search-assigned-open|jira-submit-issue|jira-submit-assigned-open|jira-add-comment|jira-refresh-auth|jira-poll-once|jira-poll-daemon",
171
203
  " sentry-get-event|sentry-submit-event",
172
204
  " Example: optimus help jira-submit-issue",
@@ -178,11 +210,7 @@ function renderCliHelp() {
178
210
  "",
179
211
  "Global options:",
180
212
  " -h, --help Show this help",
181
- " -v, --version Show the installed package version",
182
- "",
183
- "Notes:",
184
- " - `optimus` with no command prints help instead of starting runtime.",
185
- ` - Run \`optimus setup\` once; Optimus stores config and runtime data under ${resolveOptimusHomeDir()}.`
213
+ " -v, --version Show the installed package version"
186
214
  ].join("\n");
187
215
  }
188
216
  function renderCommandHelp(command) {
@@ -207,6 +235,27 @@ function renderCommandHelp(command) {
207
235
  "Example:",
208
236
  " optimus submit --title \"Login crash\" --description \"Crash on startup\" --repo ohos-pre"
209
237
  ].join("\n"),
238
+ feedback: [
239
+ "optimus feedback",
240
+ "",
241
+ "Usage:",
242
+ " optimus feedback --title <title> --description <text>",
243
+ "",
244
+ "Required:",
245
+ " --title <title> Feedback title",
246
+ " --description <text> Feedback description",
247
+ "",
248
+ "Behavior:",
249
+ " - writes a local feedback bundle under ~/.optimus/runtime/feedback",
250
+ " - creates a .tar.gz archive for the bundle",
251
+ " - attaches runtime and runtime-trace logs for the submission date when present",
252
+ ` - creates a Feishu feedback document for ${DEFAULT_FEEDBACK_RECIPIENT_EMAIL}`,
253
+ " - grants that document edit access to the fixed feedback recipient",
254
+ " - sends a Feishu app card message with the feedback document link",
255
+ "",
256
+ "Example:",
257
+ " optimus feedback --title \"Upgrade prompt missing\" --description \"upgrade --check found 0.1.14 but start did not open the upgrade prompt.\""
258
+ ].join("\n"),
210
259
  "retry-task": [
211
260
  "optimus retry-task",
212
261
  "",
@@ -301,6 +350,34 @@ function renderCommandHelp(command) {
301
350
  " --json Print JSON when used with --quick",
302
351
  " --format text Print text report"
303
352
  ].join("\n"),
353
+ advanced: renderAdvancedCliHelp(),
354
+ "notify-test": [
355
+ "optimus notify-test",
356
+ "",
357
+ "Usage:",
358
+ " optimus notify-test [options]",
359
+ "",
360
+ "Optional:",
361
+ " --channel <auto|webhook|app> Select delivery path to validate, default auto",
362
+ " --format <card|text> Override Feishu message format for this test",
363
+ " --title <title> Override message title",
364
+ " --task-id <task-id> Override synthetic task id",
365
+ " --task-type <type> Override synthetic task type",
366
+ " --outcome <value> Override synthetic outcome",
367
+ " --repo <alias> Override synthetic repo",
368
+ " --decision <text> Override decision line",
369
+ " --validation <text> Override validation line",
370
+ " --next-action <text> Override next action line",
371
+ " --print-message true Preview the final payload without dispatch",
372
+ " --print-env-status true Print Feishu runtime status only",
373
+ "",
374
+ "Examples:",
375
+ " optimus notify-test",
376
+ " optimus notify-test --channel webhook",
377
+ ` optimus notify-test --channel app --title "Optimus Feishu app smoke test"`,
378
+ " optimus notify-test --format text",
379
+ " optimus notify-test --format card --print-message true"
380
+ ].join("\n"),
304
381
  upgrade: [
305
382
  "optimus upgrade",
306
383
  "",
@@ -317,32 +394,6 @@ function renderCommandHelp(command) {
317
394
  " optimus upgrade --check --channel snapshot",
318
395
  " optimus upgrade --channel snapshot"
319
396
  ].join("\n"),
320
- feishu: [
321
- "optimus feishu auth",
322
- "",
323
- "Usage:",
324
- " optimus feishu auth login [--json]",
325
- " optimus feishu auth status [--json]",
326
- " optimus feishu auth logout",
327
- "",
328
- "Notes:",
329
- " - `login` starts Feishu device-code login for document upload and real @mentions.",
330
- " - `status` shows whether the current Optimus Feishu session is still valid.",
331
- " - `logout` clears the local Optimus Feishu auth token."
332
- ].join("\n"),
333
- "feishu auth": [
334
- "optimus feishu auth",
335
- "",
336
- "Usage:",
337
- " optimus feishu auth login [--json]",
338
- " optimus feishu auth status [--json]",
339
- " optimus feishu auth logout",
340
- "",
341
- "Notes:",
342
- " - `login` starts Feishu device-code login for document upload and real @mentions.",
343
- " - `status` shows whether the current Optimus Feishu session is still valid.",
344
- " - `logout` clears the local Optimus Feishu auth token."
345
- ].join("\n"),
346
397
  "jira-submit-issue": [
347
398
  "optimus jira-submit-issue",
348
399
  "",
@@ -696,6 +747,26 @@ async function maybeHandleStartupSelfUpdate(input) {
696
747
  interactivePromptAllowed: isInteractiveSelfUpdatePromptAllowed()
697
748
  });
698
749
  if (!check.checked) {
750
+ if (check.reason === "cached") {
751
+ const intervalHours = Math.max(1, selfUpdateConfig.checkIntervalHours);
752
+ const lastCheckedAt = check.state.lastCheckedAt ?? null;
753
+ const lastCheckedAtMs = lastCheckedAt ? Date.parse(lastCheckedAt) : Number.NaN;
754
+ const nextCheckAt = Number.isFinite(lastCheckedAtMs)
755
+ ? new Date(lastCheckedAtMs + intervalHours * 60 * 60 * 1000).toISOString()
756
+ : null;
757
+ await input.logger.info("self_update.cached", {
758
+ command: input.command,
759
+ currentVersion: check.currentVersion,
760
+ latestVersion: check.latestVersion ?? null,
761
+ channel: check.channel,
762
+ installSource: check.installSource,
763
+ intervalHours,
764
+ lastCheckedAt,
765
+ nextCheckAt,
766
+ updateAvailable: check.updateAvailable,
767
+ promptPlanned: shouldPromptCachedUpdate
768
+ });
769
+ }
699
770
  if (!shouldPromptCachedUpdate && check.reason === "cached" && check.updateAvailable && check.latestVersion) {
700
771
  console.log(`[optimus] update available ${check.currentVersion} -> ${check.latestVersion}. Run \`optimus upgrade\`.`);
701
772
  }
@@ -843,57 +914,107 @@ function buildPublicationReviewSummary(attempts) {
843
914
  }
844
915
  async function buildFeishuDoctorStatus(config) {
845
916
  const webhookConfigured = Boolean(config.delivery.feishu.webhook?.trim() || config.delivery.feishu.webhooks?.some((value) => value.trim()));
846
- const authStatus = new FeishuAuthService().readStatus();
847
- const hasAppId = Boolean(config.delivery.feishu.appId?.trim() || process.env.FEISHU_APP_ID?.trim());
848
- const hasAppSecret = Boolean(config.delivery.feishu.appSecret?.trim() || process.env.FEISHU_APP_SECRET?.trim());
849
- const appCredentialConfigured = hasAppId && hasAppSecret;
850
- const authReady = authStatus.authenticated || appCredentialConfigured;
851
- const authMode = authStatus.authenticated
852
- ? "user_access_token"
853
- : appCredentialConfigured
854
- ? "app_credentials"
855
- : "none";
917
+ const client = new FeishuClient({ config });
918
+ const userService = new FeishuUserService({ config });
919
+ const appReady = client.isConfigured();
920
+ const mentionReady = userService.isConfigured();
856
921
  return {
857
922
  webhook: {
858
923
  configured: webhookConfigured,
859
924
  signed: Boolean(config.delivery.feishu.secret?.trim())
860
925
  },
861
926
  auth: {
862
- configured: authReady,
863
- baseUrl: authStatus.baseUrl,
864
- mode: authMode,
865
- accessTokenExpired: authStatus.accessTokenExpired,
866
- refreshTokenExpired: authStatus.refreshTokenExpired,
867
- ...(authStatus.userId ? { userId: authStatus.userId } : {}),
868
- ...(authStatus.openId ? { openId: authStatus.openId } : {}),
869
- ...(authStatus.accessTokenExpiresAt ? { accessTokenExpiresAt: authStatus.accessTokenExpiresAt } : {}),
870
- ...(authStatus.refreshTokenExpiresAt ? { refreshTokenExpiresAt: authStatus.refreshTokenExpiresAt } : {}),
871
- detail: authStatus.authenticated
872
- ? "Feishu auth token is ready for document upload and real @mentions."
873
- : appCredentialConfigured
874
- ? "Feishu runtime auth is available for document upload and user lookup."
875
- : "Feishu webhook delivery can still work, but document upload and real @mentions are unavailable until Optimus signs in to Feishu.",
876
- ...(authReady ? {} : { recommendation: "Run `optimus feishu auth login` to enable analysis docs and real @mentions." })
927
+ configured: appReady,
928
+ baseUrl: config.delivery.feishu.baseUrl?.trim() || process.env.FEISHU_BASE_URL?.trim() || "https://open.feishu.cn",
929
+ mode: "builtin_app",
930
+ detail: appReady
931
+ ? "Built-in Optimus Feishu app is ready for message delivery, document publishing, and capability probes."
932
+ : "Feishu app delivery is unavailable."
877
933
  },
878
934
  mentions: {
879
935
  requiresOpenId: true,
880
- ready: authReady,
881
- detail: authReady
882
- ? "Authenticated Feishu user lookup is ready for real @mentions."
883
- : "Real @mentions require `optimus feishu auth login`.",
884
- ...(authReady ? {} : { recommendation: "Run `optimus feishu auth login`." })
936
+ ready: mentionReady,
937
+ mode: mentionReady ? "app_identity" : "unavailable",
938
+ detail: mentionReady
939
+ ? "App-identity user lookup is ready for private delivery and real @mentions."
940
+ : "Real @mentions require Feishu app availability.",
941
+ ...(mentionReady
942
+ ? {}
943
+ : { recommendation: "Rerun `optimus doctor --quick` after restoring Feishu app availability." })
885
944
  }
886
945
  };
887
946
  }
888
- function buildFeishuRuntimeStatus(config) {
889
- const authStatus = new FeishuAuthService().readStatus();
947
+ function buildFeishuRuntimeStatus(config, env = process.env) {
948
+ const client = new FeishuClient({ config, env });
949
+ return {
950
+ webhook: Boolean(config.delivery.feishu.webhook?.trim()),
951
+ webhooks: Boolean(config.delivery.feishu.webhooks?.some((value) => value.trim())),
952
+ secret: Boolean(config.delivery.feishu.secret?.trim() || env.OPTIMUS_FEISHU_SECRET?.trim()),
953
+ auth: client.isConfigured()
954
+ };
955
+ }
956
+ function buildNotifyTestConfig(config, channel) {
957
+ if (channel === "auto") {
958
+ return config;
959
+ }
960
+ if (channel === "webhook") {
961
+ return {
962
+ ...config,
963
+ delivery: {
964
+ ...config.delivery,
965
+ feishu: {
966
+ ...config.delivery.feishu
967
+ }
968
+ }
969
+ };
970
+ }
890
971
  return {
891
- webhook: Boolean(config.delivery.feishu.webhook?.trim() || process.env.OPTIMUS_FEISHU_WEBHOOK?.trim()),
892
- webhooks: Boolean(config.delivery.feishu.webhooks?.some((value) => value.trim()) || process.env.OPTIMUS_FEISHU_WEBHOOKS?.trim()),
893
- secret: Boolean(config.delivery.feishu.secret?.trim() || process.env.OPTIMUS_FEISHU_SECRET?.trim()),
894
- auth: authStatus.authenticated || Boolean((config.delivery.feishu.appId?.trim() || process.env.FEISHU_APP_ID?.trim()) && (config.delivery.feishu.appSecret?.trim() || process.env.FEISHU_APP_SECRET?.trim()))
972
+ ...config,
973
+ delivery: {
974
+ ...config.delivery,
975
+ feishu: {
976
+ ...config.delivery.feishu,
977
+ webhook: "",
978
+ webhooks: []
979
+ }
980
+ }
895
981
  };
896
982
  }
983
+ function buildNotifyTestEnv(channel) {
984
+ return process.env;
985
+ }
986
+ function resolveNotifyTestDefaultTitle(channel) {
987
+ if (channel === "app") {
988
+ return "Optimus Feishu app smoke test";
989
+ }
990
+ return "Optimus Feishu webhook smoke test";
991
+ }
992
+ function buildNotifyTestInvocationId(now = new Date()) {
993
+ const timestamp = now.toISOString().replace(/[-:TZ.]/gu, "").slice(0, 14);
994
+ const randomSuffix = Math.random().toString(36).slice(2, 6);
995
+ return `${timestamp}-${randomSuffix}`;
996
+ }
997
+ function resolveNotifyTestDefaultDecision(channel) {
998
+ if (channel === "app") {
999
+ return "Manual Feishu app smoke test.";
1000
+ }
1001
+ return "Manual webhook smoke test.";
1002
+ }
1003
+ function resolveNotifyTestDefaultNextAction(channel) {
1004
+ if (channel === "app") {
1005
+ return "If this arrives, app delivery is working.";
1006
+ }
1007
+ return "If this arrives, webhook delivery is working.";
1008
+ }
1009
+ function isNotifyTestEnvReady(envStatus, channel) {
1010
+ if (channel === "app") {
1011
+ return envStatus.auth;
1012
+ }
1013
+ if (channel === "webhook") {
1014
+ return envStatus.webhook || envStatus.webhooks;
1015
+ }
1016
+ return envStatus.webhook || envStatus.webhooks || envStatus.auth;
1017
+ }
897
1018
  function extractCommandFailureDetail(error) {
898
1019
  if (!(error instanceof Error)) {
899
1020
  return typeof error === "string" ? error : undefined;
@@ -911,7 +1032,7 @@ async function promptSetupAnswers(defaults) {
911
1032
  let scriptedIndex = 0;
912
1033
  const ask = async (prompt) => {
913
1034
  if (scriptedAnswers) {
914
- output.write(prompt);
1035
+ output.write(`${prompt}\n`);
915
1036
  const answer = scriptedAnswers[scriptedIndex] ?? "";
916
1037
  scriptedIndex += 1;
917
1038
  return answer;
@@ -924,18 +1045,38 @@ async function promptSetupAnswers(defaults) {
924
1045
  console.log(detail);
925
1046
  }
926
1047
  };
1048
+ const printSetupHint = (detail) => {
1049
+ console.log(` -> ${detail}`);
1050
+ };
927
1051
  const renderSetupPrompt = (label, promptLabel, defaultValue) => {
928
1052
  const suffix = defaultValue !== undefined ? ` [${defaultValue}]` : "";
929
1053
  return `[${label}] ${promptLabel}${suffix}: `;
930
1054
  };
1055
+ const resolveOptionalTextAnswer = (rawAnswer, defaultValue) => {
1056
+ const trimmed = rawAnswer.trim();
1057
+ if (trimmed === '""' || trimmed === "''") {
1058
+ return null;
1059
+ }
1060
+ if (trimmed.length === 0) {
1061
+ return defaultValue;
1062
+ }
1063
+ return trimmed;
1064
+ };
931
1065
  try {
1066
+ console.log("Optimus setup");
1067
+ console.log("This usually takes about 2 minutes.");
1068
+ console.log("Required: repository + Codex auth. Optional: Feishu webhook, Jira, Sentry.");
1069
+ console.log("You can rerun `optimus setup` later to change anything.");
932
1070
  if (await pathExists(configPath)) {
933
1071
  console.log(`Existing config found at ${configPath}. Press Enter to keep current values.`);
934
1072
  }
935
- printSetupSection("Repository", "Register the primary local repository used by Optimus.");
1073
+ printSetupSection("Repository", "Point Optimus at the main local repository it should work in.");
1074
+ printSetupHint("Repository path: the local code directory Optimus will read and edit.");
936
1075
  const repoPath = (await ask(renderSetupPrompt("Required", "Repository path", defaults.repoPath))).trim() || defaults.repoPath;
1076
+ printSetupHint("Repository alias: the short name used later in commands like --repo ohos-pre.");
937
1077
  const repoAlias = (await ask(renderSetupPrompt("Required", "Repository alias", defaults.repoAlias))).trim() || defaults.repoAlias;
938
- printSetupSection("Codex", "Choose one authentication mode, then fill the fields that follow.");
1078
+ printSetupSection("Codex", "Choose one model authentication mode. This is required before tasks can run.");
1079
+ printSetupHint("Codex auth mode: choose how Optimus reaches the model you want to use.");
939
1080
  const codexAuthMode = await askSetupCodexAuthMode({ ask, defaultMode: defaults.codexAuthMode });
940
1081
  let codexApiKey;
941
1082
  let codexProviderId;
@@ -943,54 +1084,56 @@ async function promptSetupAnswers(defaults) {
943
1084
  let codexProviderBaseUrl;
944
1085
  let codexProviderApiKeyEnvName;
945
1086
  if (codexAuthMode === "openai_api_key") {
1087
+ printSetupHint("OpenAI API key: used only when you choose direct OpenAI API authentication.");
946
1088
  codexApiKey = (await ask(renderSetupPrompt("Optional", "OpenAI API key for OPENAI_API_KEY", "leave blank to keep existing"))).trim() || undefined;
947
1089
  }
948
1090
  else if (codexAuthMode === "model_provider") {
1091
+ printSetupHint("Provider id: a short stable id for this model provider, for example palebluedot.");
949
1092
  codexProviderId = (await ask(renderSetupPrompt("Required", "Provider id", defaults.codexProviderId ?? ""))).trim() || defaults.codexProviderId;
1093
+ printSetupHint("Provider display name: what Optimus shows in status and doctor output.");
950
1094
  codexProviderDisplayName = (await ask(renderSetupPrompt("Required", "Provider display name", defaults.codexProviderDisplayName ?? ""))).trim() || defaults.codexProviderDisplayName;
1095
+ printSetupHint("Provider base URL: the model API endpoint root Optimus should call.");
951
1096
  codexProviderBaseUrl = (await ask(renderSetupPrompt("Required", "Provider base URL", defaults.codexProviderBaseUrl ?? ""))).trim() || defaults.codexProviderBaseUrl;
1097
+ printSetupHint("Provider API key env name: the environment variable name Optimus will read at runtime.");
952
1098
  codexProviderApiKeyEnvName = (await ask(renderSetupPrompt("Required", "Provider API key env name", defaults.codexProviderApiKeyEnvName ?? ""))).trim() || defaults.codexProviderApiKeyEnvName;
953
1099
  const providerKeyLabel = codexProviderApiKeyEnvName || defaults.codexProviderApiKeyEnvName || "the configured provider env";
1100
+ printSetupHint(`Provider API key value: optional here; leave blank to keep the current ${providerKeyLabel} value.`);
954
1101
  codexApiKey = (await ask(renderSetupPrompt("Optional", `Provider API key value for ${providerKeyLabel}`, "leave blank to keep existing"))).trim() || undefined;
955
1102
  }
956
- printSetupSection("Feishu", "Enable this module when task notifications should be sent to Feishu. Use `optimus feishu auth login` later for analysis docs and real @mentions.");
957
- const enableFeishuInput = (await ask(renderSetupPrompt("Optional module", "Enable Feishu delivery", defaults.enableFeishu ? "Y/n" : "y/N"))).trim().toLowerCase();
958
- const enableFeishu = enableFeishuInput.length === 0
959
- ? defaults.enableFeishu
960
- : ["y", "yes"].includes(enableFeishuInput);
961
- const feishuWebhook = enableFeishu
962
- ? (await ask(renderSetupPrompt("Required", "Feishu webhook", defaults.feishuWebhook ? "configured" : ""))).trim() || defaults.feishuWebhook
963
- : undefined;
964
- const feishuSecret = enableFeishu
965
- ? (await ask(renderSetupPrompt("Optional", "Feishu secret", defaults.feishuSecret ? "configured" : ""))).trim() || defaults.feishuSecret
966
- : undefined;
967
- printSetupSection("Jira", "Enable this module when Optimus should read issues or comment back to Jira.");
968
- const enableJiraInput = (await ask(renderSetupPrompt("Optional module", "Enable Jira integration", defaults.enableJira ? "Y/n" : "y/N"))).trim().toLowerCase();
1103
+ printSetupSection("Feishu", "Built-in Feishu app delivery is already available. Add a webhook only if group notifications should go to a chat first. Type \"\" to clear an existing optional value.");
1104
+ printSetupHint("Feishu webhook: optional; leave empty to use direct Feishu app delivery only.");
1105
+ const feishuWebhook = resolveOptionalTextAnswer(await ask(renderSetupPrompt("Optional", "Feishu webhook", defaults.feishuWebhook ? "configured" : "")), defaults.feishuWebhook ?? undefined);
1106
+ printSetupHint("Feishu secret: optional; only needed when the webhook itself requires signature verification.");
1107
+ const feishuSecret = resolveOptionalTextAnswer(await ask(renderSetupPrompt("Optional", "Feishu secret", defaults.feishuSecret ? "configured" : "")), defaults.feishuSecret ?? undefined);
1108
+ printSetupSection("Jira", "Optional. Turn this on only if you want this machine to read Jira issues or write Jira comments.");
1109
+ printSetupHint("If disabled, Optimus still works; Jira commands just stay unavailable on this machine.");
1110
+ const enableJiraInput = (await ask(renderSetupPrompt("Optional integration", "Enable Jira integration", defaults.enableJira ? "Y/n" : "y/N"))).trim().toLowerCase();
969
1111
  const enableJira = enableJiraInput.length === 0
970
1112
  ? defaults.enableJira
971
1113
  : ["y", "yes"].includes(enableJiraInput);
972
1114
  const jiraBaseUrl = enableJira
973
- ? (await ask(renderSetupPrompt("Required", "Jira base URL", defaults.jiraBaseUrl ?? ""))).trim() || defaults.jiraBaseUrl
1115
+ ? (printSetupHint("Jira base URL: the Jira site root, for example https://jira.example.com/."), (await ask(renderSetupPrompt("Required", "Jira base URL", defaults.jiraBaseUrl ?? ""))).trim() || defaults.jiraBaseUrl)
974
1116
  : undefined;
975
1117
  const jiraPersonalToken = enableJira
976
- ? (await ask(renderSetupPrompt("Required", "Jira personal token", defaults.jiraPersonalToken ? "configured" : ""))).trim() || defaults.jiraPersonalToken
1118
+ ? (printSetupHint("Jira personal token: required for Jira reads, comments, and issue submission commands."), (await ask(renderSetupPrompt("Required", "Jira personal token", defaults.jiraPersonalToken ? "configured" : ""))).trim() || defaults.jiraPersonalToken)
977
1119
  : undefined;
978
1120
  const jiraCasCookieName = enableJira
979
- ? (await ask(renderSetupPrompt("Optional", "Jira CAS cookie name", defaults.jiraCasCookieName ?? "_aegis_cas_p"))).trim() || defaults.jiraCasCookieName || "_aegis_cas_p"
1121
+ ? (printSetupHint("Jira CAS cookie name: optional helper for environments that need refreshed CAS login cookies."), (await ask(renderSetupPrompt("Optional", "Jira CAS cookie name", defaults.jiraCasCookieName ?? "_aegis_cas_p"))).trim() || defaults.jiraCasCookieName || "_aegis_cas_p")
980
1122
  : undefined;
981
- printSetupSection("Sentry", "Enable this module when tasks can be created from Sentry events.");
982
- const enableSentryInput = (await ask(renderSetupPrompt("Optional module", "Enable Sentry integration", defaults.enableSentry ? "Y/n" : "y/N"))).trim().toLowerCase();
1123
+ printSetupSection("Sentry", "Optional. Turn this on only if you want this machine to read Sentry events and create tasks from them.");
1124
+ printSetupHint("If disabled, Optimus still works; Sentry commands just stay unavailable on this machine.");
1125
+ const enableSentryInput = (await ask(renderSetupPrompt("Optional integration", "Enable Sentry integration", defaults.enableSentry ? "Y/n" : "y/N"))).trim().toLowerCase();
983
1126
  const enableSentry = enableSentryInput.length === 0
984
1127
  ? defaults.enableSentry
985
1128
  : ["y", "yes"].includes(enableSentryInput);
986
1129
  const sentryBaseUrl = enableSentry
987
- ? (await ask(renderSetupPrompt("Required", "Sentry base URL", defaults.sentryBaseUrl ?? ""))).trim() || defaults.sentryBaseUrl
1130
+ ? (printSetupHint("Sentry base URL: the Sentry site root, for example https://sentry.example.com/."), (await ask(renderSetupPrompt("Required", "Sentry base URL", defaults.sentryBaseUrl ?? ""))).trim() || defaults.sentryBaseUrl)
988
1131
  : undefined;
989
1132
  const sentryOrg = enableSentry
990
- ? (await ask(renderSetupPrompt("Required", "Sentry org", defaults.sentryOrg ?? ""))).trim() || defaults.sentryOrg
1133
+ ? (printSetupHint("Sentry org: the organization slug used in Sentry API requests."), (await ask(renderSetupPrompt("Required", "Sentry org", defaults.sentryOrg ?? ""))).trim() || defaults.sentryOrg)
991
1134
  : undefined;
992
1135
  const sentryAuthToken = enableSentry
993
- ? (await ask(renderSetupPrompt("Required", "Sentry auth token", defaults.sentryAuthToken ? "configured" : ""))).trim() || defaults.sentryAuthToken
1136
+ ? (printSetupHint("Sentry auth token: required if this machine should read events from Sentry."), (await ask(renderSetupPrompt("Required", "Sentry auth token", defaults.sentryAuthToken ? "configured" : ""))).trim() || defaults.sentryAuthToken)
994
1137
  : undefined;
995
1138
  return {
996
1139
  repoPath,
@@ -1001,9 +1144,8 @@ async function promptSetupAnswers(defaults) {
1001
1144
  ...(codexProviderDisplayName ? { codexProviderDisplayName } : {}),
1002
1145
  ...(codexProviderBaseUrl ? { codexProviderBaseUrl } : {}),
1003
1146
  ...(codexProviderApiKeyEnvName ? { codexProviderApiKeyEnvName } : {}),
1004
- enableFeishu,
1005
- ...(feishuWebhook ? { feishuWebhook } : {}),
1006
- ...(feishuSecret ? { feishuSecret } : {}),
1147
+ ...(feishuWebhook !== undefined ? { feishuWebhook } : {}),
1148
+ ...(feishuSecret !== undefined ? { feishuSecret } : {}),
1007
1149
  enableJira,
1008
1150
  ...(jiraBaseUrl ? { jiraBaseUrl } : {}),
1009
1151
  ...(jiraPersonalToken ? { jiraPersonalToken } : {}),
@@ -1034,7 +1176,6 @@ async function resolveDefaultSetupAnswers() {
1034
1176
  repoPath: "",
1035
1177
  repoAlias: "",
1036
1178
  codexAuthMode: defaults.codex.auth.mode,
1037
- enableFeishu: false,
1038
1179
  enableJira: false,
1039
1180
  enableSentry: false
1040
1181
  };
@@ -1054,7 +1195,6 @@ async function resolveDefaultSetupAnswers() {
1054
1195
  ...(provider?.displayName ? { codexProviderDisplayName: provider.displayName } : {}),
1055
1196
  ...(provider?.baseUrl ? { codexProviderBaseUrl: provider.baseUrl } : {}),
1056
1197
  ...(provider?.apiKeyEnvName ? { codexProviderApiKeyEnvName: provider.apiKeyEnvName } : {}),
1057
- enableFeishu: config.delivery.channels.includes("feishu"),
1058
1198
  ...(config.delivery.feishu.webhook ? { feishuWebhook: config.delivery.feishu.webhook } : {}),
1059
1199
  ...(config.delivery.feishu.secret ? { feishuSecret: config.delivery.feishu.secret } : {}),
1060
1200
  enableJira: config.jira.enabled,
@@ -1126,9 +1266,6 @@ function validateSetupAnswers(answers) {
1126
1266
  if (answers.codexAuthMode === "model_provider" && !answers.codexProviderApiKeyEnvName?.trim()) {
1127
1267
  return "setup requires provider API key env name when Codex auth mode is model_provider.";
1128
1268
  }
1129
- if (answers.enableFeishu && !answers.feishuWebhook?.trim()) {
1130
- return "setup requires a Feishu webhook when Feishu delivery is enabled.";
1131
- }
1132
1269
  if (answers.enableJira && !answers.jiraBaseUrl?.trim()) {
1133
1270
  return "setup requires a Jira base URL when Jira integration is enabled.";
1134
1271
  }
@@ -1244,12 +1381,19 @@ function buildSetupConfig(answers, rawConfig) {
1244
1381
  else {
1245
1382
  delete config.codex.setupCache;
1246
1383
  }
1247
- config.delivery.channels = answers.enableFeishu ? ["console", "feishu"] : ["console"];
1384
+ config.delivery.channels = ["console", "feishu"];
1248
1385
  config.delivery.feishu = {
1249
1386
  ...config.delivery.feishu,
1250
- ...(answers.enableFeishu && answers.feishuWebhook ? { webhook: answers.feishuWebhook } : {}),
1251
- ...(answers.enableFeishu && answers.feishuSecret ? { secret: answers.feishuSecret } : {})
1387
+ ...(typeof answers.feishuWebhook === "string" ? { webhook: answers.feishuWebhook } : {}),
1388
+ ...(typeof answers.feishuSecret === "string" ? { secret: answers.feishuSecret } : {})
1252
1389
  };
1390
+ if (answers.feishuWebhook === null) {
1391
+ delete config.delivery.feishu.webhook;
1392
+ delete config.delivery.feishu.webhooks;
1393
+ }
1394
+ if (answers.feishuSecret === null) {
1395
+ delete config.delivery.feishu.secret;
1396
+ }
1253
1397
  config.jira.enabled = answers.enableJira;
1254
1398
  if (answers.enableJira) {
1255
1399
  config.jira.baseUrl = answers.jiraBaseUrl ?? config.jira.baseUrl;
@@ -1456,31 +1600,12 @@ async function runQuickDoctor(configPath = resolveDefaultConfigPath()) {
1456
1600
  next.add("git config user.email \"you@example.com\"");
1457
1601
  }
1458
1602
  }
1459
- const feishuEnabled = config.delivery.channels.includes("feishu");
1460
- const configuredFeishuTargets = [
1461
- config.delivery.feishu.webhook?.trim(),
1462
- ...(config.delivery.feishu.webhooks ?? []).map((value) => value.trim())
1463
- ].filter((value) => Boolean(value));
1464
- if (feishuEnabled && configuredFeishuTargets.length === 0) {
1465
- blocking.push({
1466
- code: "feishu_webhook_missing",
1467
- message: "Feishu delivery is enabled, but no webhook is configured.",
1468
- fix: "Rerun `optimus setup` and provide a Feishu webhook."
1469
- });
1470
- next.add("optimus setup");
1471
- }
1472
- if (!feishuEnabled) {
1603
+ const feishuEnabled = config.delivery.enabled && config.delivery.channels.includes("feishu");
1604
+ if (!config.delivery.enabled) {
1473
1605
  warnings.push({
1474
1606
  code: "feishu_disabled",
1475
- message: "Feishu delivery is disabled.",
1476
- impact: "Notifications will stay console-only."
1477
- });
1478
- }
1479
- else if (!new FeishuAuthService().readStatus().authenticated && (!config.delivery.feishu.appId?.trim() || !config.delivery.feishu.appSecret?.trim())) {
1480
- warnings.push({
1481
- code: "feishu_auth_missing",
1482
- message: "Feishu auth token is missing.",
1483
- impact: "Webhook messages can still be sent, but analysis document upload and real @mentions will be skipped."
1607
+ message: "Task delivery is disabled.",
1608
+ impact: "Console, Feishu, and analysis-document delivery are all paused."
1484
1609
  });
1485
1610
  }
1486
1611
  if (config.jira.enabled) {
@@ -1606,15 +1731,25 @@ function inferArtifactDir(artifacts, bundle) {
1606
1731
  }
1607
1732
  return dirname(artifactPath);
1608
1733
  }
1609
- function inferTaskRootDir(artifactDir) {
1734
+ function inferTaskRootDir(artifactDir, fallbackTaskRootDir) {
1610
1735
  if (!artifactDir) {
1611
- return undefined;
1736
+ return fallbackTaskRootDir;
1737
+ }
1738
+ if (fallbackTaskRootDir) {
1739
+ const resolvedArtifactDir = resolve(artifactDir);
1740
+ const resolvedFallbackTaskRootDir = resolve(fallbackTaskRootDir);
1741
+ if (resolvedArtifactDir === resolvedFallbackTaskRootDir
1742
+ || resolvedArtifactDir.startsWith(`${resolvedFallbackTaskRootDir}${sep}`)) {
1743
+ return resolve(artifactDir, "..", "..");
1744
+ }
1745
+ return resolvedFallbackTaskRootDir;
1612
1746
  }
1613
1747
  return resolve(artifactDir, "..", "..");
1614
1748
  }
1615
1749
  async function buildReplayExecutionContext(input) {
1616
1750
  const artifactDir = inferArtifactDir(input.artifacts, input.deliveryBundle);
1617
- const taskRootDir = inferTaskRootDir(artifactDir);
1751
+ const fallbackTaskRootDir = join(input.config.storage.rootDir, "workspaces", input.taskId);
1752
+ const taskRootDir = inferTaskRootDir(artifactDir, fallbackTaskRootDir);
1618
1753
  const sourceDir = input.deliveryBundle?.publication?.git?.repoDir?.trim()
1619
1754
  || input.latestRun?.addresses?.visibleRepoDir?.trim()
1620
1755
  || input.latestRun?.addresses?.workspaceDir?.trim();
@@ -1870,56 +2005,7 @@ async function main() {
1870
2005
  return;
1871
2006
  }
1872
2007
  if (command === "feishu") {
1873
- const [firstSubcommand, secondSubcommand] = commandArgs;
1874
- const args = parseArgs(commandArgs.slice(2));
1875
- if (firstSubcommand !== "auth" || !secondSubcommand) {
1876
- console.log(renderCommandHelp("feishu") ?? renderCliHelp());
1877
- process.exitCode = 1;
1878
- return;
1879
- }
1880
- const authService = new FeishuAuthService();
1881
- if (secondSubcommand === "status") {
1882
- const status = authService.readStatus();
1883
- if (args.json === "true") {
1884
- console.log(JSON.stringify(status, null, 2));
1885
- }
1886
- else if (status.authenticated) {
1887
- console.log(`Feishu auth is ready. user=${status.userId ?? "unknown"} open_id=${status.openId ?? "unknown"} access_token_expires=${status.accessTokenExpiresAt ?? "unknown"} refresh_token_expires=${status.refreshTokenExpiresAt ?? "unknown"}`);
1888
- }
1889
- else {
1890
- console.log("Feishu auth is not ready. Run `optimus feishu auth login`.");
1891
- }
1892
- process.exitCode = status.authenticated ? 0 : 1;
1893
- return;
1894
- }
1895
- if (secondSubcommand === "login") {
1896
- const result = await authService.login({
1897
- onPrompt: ({ verificationUrl, expiresInSeconds }) => {
1898
- console.error(`Open this URL to authorize:\n${verificationUrl}\n`);
1899
- console.error(`Waiting for authorization (expires in ${expiresInSeconds}s)...`);
1900
- }
1901
- });
1902
- if (args.json === "true") {
1903
- console.log(JSON.stringify({
1904
- ok: true,
1905
- userId: result.userId ?? null,
1906
- openId: result.openId ?? null,
1907
- verificationUrl: result.verificationUrl,
1908
- accessTokenExpiresAt: new Date(result.expiresAtMs).toISOString(),
1909
- refreshTokenExpiresAt: new Date(result.refreshExpiresAtMs).toISOString()
1910
- }, null, 2));
1911
- }
1912
- else {
1913
- console.log(`Feishu auth succeeded. user=${result.userId ?? "unknown"} open_id=${result.openId ?? "unknown"}`);
1914
- }
1915
- return;
1916
- }
1917
- if (secondSubcommand === "logout") {
1918
- await authService.logout();
1919
- console.log("Feishu auth cleared.");
1920
- return;
1921
- }
1922
- console.log(renderCommandHelp("feishu") ?? renderCliHelp());
2008
+ console.log("Feishu user-session commands have been removed. Optimus now uses the built-in Feishu app for delivery and document publishing.");
1923
2009
  process.exitCode = 1;
1924
2010
  return;
1925
2011
  }
@@ -1949,7 +2035,13 @@ async function main() {
1949
2035
  }
1950
2036
  catch (error) {
1951
2037
  if (error?.code === "ENOENT") {
1952
- console.error("Configuration is missing. Run `optimus setup` first.");
2038
+ console.error([
2039
+ "Optimus is not set up on this machine yet.",
2040
+ "Next:",
2041
+ " 1. optimus setup",
2042
+ " 2. optimus start",
2043
+ " 3. optimus submit --title \"Login crash\" --description \"Crash on startup\""
2044
+ ].join("\n"));
1953
2045
  process.exitCode = 1;
1954
2046
  return;
1955
2047
  }
@@ -2023,6 +2115,42 @@ async function main() {
2023
2115
  console.log(`[optimus] wrote submission ${inboxPath}`);
2024
2116
  return;
2025
2117
  }
2118
+ if (command === "feedback") {
2119
+ const args = parseArgs(commandArgs);
2120
+ const title = args.title?.trim();
2121
+ const description = (args.desc ?? args.description)?.trim();
2122
+ if (!title || !description) {
2123
+ console.error("feedback requires both --title and --description");
2124
+ process.exitCode = 1;
2125
+ return;
2126
+ }
2127
+ const result = await createFeedbackReport({
2128
+ title,
2129
+ description,
2130
+ currentVersion,
2131
+ packageRoot: CLI_ROOT_DIR
2132
+ });
2133
+ console.log("[optimus] feedback saved locally");
2134
+ console.log(`id: ${result.id}`);
2135
+ console.log(`path: ${result.feedbackDir}`);
2136
+ console.log(`report: ${result.reportPath}`);
2137
+ console.log(`archive: ${result.archivePath}`);
2138
+ console.log(`logs: ${result.files.filter((file) => file.found).length}/${result.files.length}`);
2139
+ const feedbackDelivery = new FeishuFeedbackDeliveryService({
2140
+ config
2141
+ });
2142
+ const deliveryResult = await feedbackDelivery.deliver({
2143
+ title,
2144
+ description,
2145
+ currentVersion,
2146
+ report: result
2147
+ });
2148
+ console.log(`recipient: ${deliveryResult.recipientEmail}`);
2149
+ console.log(`open_id: ${deliveryResult.recipientOpenId}`);
2150
+ console.log(`doc: ${deliveryResult.document.status}${deliveryResult.document.url ? ` (${deliveryResult.document.url})` : deliveryResult.document.reason ? ` (${deliveryResult.document.reason})` : ""}`);
2151
+ console.log(`notify: ${deliveryResult.notification.status}${deliveryResult.notification.reason ? ` (${deliveryResult.notification.reason})` : ""}`);
2152
+ return;
2153
+ }
2026
2154
  if (command === "retry-task") {
2027
2155
  const args = parseArgs(commandArgs);
2028
2156
  const taskId = args["task-id"];
@@ -2044,17 +2172,32 @@ async function main() {
2044
2172
  }
2045
2173
  if (command === "notify-test") {
2046
2174
  const args = parseArgs(commandArgs);
2047
- const title = args.title?.trim() || "Optimus Feishu webhook smoke test";
2048
- const taskId = args["task-id"]?.trim() || "task-feishu-smoke";
2175
+ const requestedChannel = args.channel?.trim().toLowerCase();
2176
+ if (requestedChannel && requestedChannel !== "auto" && requestedChannel !== "webhook" && requestedChannel !== "app") {
2177
+ console.error("notify-test only supports --channel auto, webhook, or app");
2178
+ process.exitCode = 1;
2179
+ return;
2180
+ }
2181
+ const requestedFormat = args.format?.trim().toLowerCase();
2182
+ if (requestedFormat && requestedFormat !== "text" && requestedFormat !== "card") {
2183
+ console.error("notify-test only supports --format text or --format card");
2184
+ process.exitCode = 1;
2185
+ return;
2186
+ }
2187
+ const notifyChannel = requestedChannel === "webhook" || requestedChannel === "app" ? requestedChannel : "auto";
2188
+ const invocationId = buildNotifyTestInvocationId();
2189
+ const title = args.title?.trim() || `${resolveNotifyTestDefaultTitle(notifyChannel)} [${invocationId}]`;
2190
+ const taskId = args["task-id"]?.trim() || `task-feishu-smoke-${invocationId}`;
2191
+ const runId = `run-feishu-smoke-${invocationId}`;
2049
2192
  const taskType = args["task-type"]?.trim() || "bugfix";
2050
2193
  const outcome = args.outcome?.trim() || "analysis_only";
2051
2194
  const repo = args.repo?.trim() || "optimus";
2052
- const decision = args.decision?.trim() || "Manual webhook smoke test.";
2195
+ const decision = args.decision?.trim() || resolveNotifyTestDefaultDecision(notifyChannel);
2053
2196
  const validation = args.validation?.trim() || "Triggered from local CLI.";
2054
- const nextAction = args["next-action"]?.trim() || "If this arrives, webhook delivery is working.";
2197
+ const nextAction = args["next-action"]?.trim() || resolveNotifyTestDefaultNextAction(notifyChannel);
2055
2198
  const bundle = {
2056
2199
  taskId,
2057
- runId: "run-feishu-smoke",
2200
+ runId,
2058
2201
  taskType,
2059
2202
  outcome: outcome,
2060
2203
  createdAt: new Date().toISOString(),
@@ -2063,7 +2206,13 @@ async function main() {
2063
2206
  repo,
2064
2207
  decision,
2065
2208
  validation,
2066
- nextAction
2209
+ nextAction,
2210
+ ...(notifyChannel === "app" ? {
2211
+ qAssignee: {
2212
+ displayName: "wangkai39",
2213
+ email: DEFAULT_FEEDBACK_RECIPIENT_EMAIL
2214
+ }
2215
+ } : {})
2067
2216
  },
2068
2217
  artifacts: {
2069
2218
  extras: []
@@ -2078,7 +2227,7 @@ async function main() {
2078
2227
  const publicationAttempts = args["review-url"] || args["change-id"]
2079
2228
  ? [{
2080
2229
  taskId,
2081
- runId: "run-feishu-smoke",
2230
+ runId,
2082
2231
  mode: "review_submit",
2083
2232
  status: "submitted",
2084
2233
  summary: args["review-summary"]?.trim() || "Review submission completed for 1/1 target(s).",
@@ -2087,12 +2236,50 @@ async function main() {
2087
2236
  ...(changeIds.length > 0 ? { changeId: changeIds[0], changeIds } : {})
2088
2237
  }]
2089
2238
  : [];
2090
- const reviewAwareNotifier = new FeishuNotifier({ config, publicationAttempts, format: config.delivery.feishu.format });
2239
+ const effectiveFormat = requestedFormat === "text" || requestedFormat === "card"
2240
+ ? requestedFormat
2241
+ : config.delivery.feishu.format;
2242
+ const notifyConfig = buildNotifyTestConfig(config, notifyChannel);
2243
+ const notifyEnv = buildNotifyTestEnv(notifyChannel);
2244
+ let appResolution;
2245
+ if (notifyChannel === "app" && args["print-message"] !== "true") {
2246
+ const assigneeEmail = bundle.summary.qAssignee?.email?.trim();
2247
+ appResolution = assigneeEmail ? { requestedEmail: assigneeEmail } : {};
2248
+ if (assigneeEmail) {
2249
+ try {
2250
+ const resolvedOpenId = await new FeishuUserService({ config: notifyConfig }).resolveOpenId(assigneeEmail);
2251
+ if (resolvedOpenId && bundle.summary.qAssignee) {
2252
+ bundle.summary.qAssignee.openId = resolvedOpenId;
2253
+ appResolution.resolvedOpenId = resolvedOpenId;
2254
+ }
2255
+ }
2256
+ catch (error) {
2257
+ appResolution.error = extractCommandFailureDetail(error) ?? String(error);
2258
+ // Keep the smoke test non-blocking and let the notifier report the final delivery status.
2259
+ }
2260
+ if (!bundle.summary.qAssignee?.openId) {
2261
+ const fallbackOpenId = resolveDefaultFeedbackRecipientFallbackOpenId(notifyEnv, notifyConfig);
2262
+ if (fallbackOpenId && bundle.summary.qAssignee) {
2263
+ bundle.summary.qAssignee.openId = fallbackOpenId;
2264
+ appResolution.resolvedOpenId = fallbackOpenId;
2265
+ if (!appResolution.error) {
2266
+ appResolution.error = "lookup_unavailable_used_fallback_open_id";
2267
+ }
2268
+ }
2269
+ }
2270
+ }
2271
+ }
2272
+ const reviewAwareNotifier = new FeishuNotifier({
2273
+ config: notifyConfig,
2274
+ env: notifyEnv,
2275
+ publicationAttempts,
2276
+ format: effectiveFormat
2277
+ });
2091
2278
  const message = reviewAwareNotifier.previewMessage(bundle);
2092
- const envStatus = buildFeishuRuntimeStatus(config);
2279
+ const envStatus = buildFeishuRuntimeStatus(notifyConfig, notifyEnv);
2093
2280
  if (args["print-env-status"] === "true") {
2094
- console.log(JSON.stringify({ envStatus }, null, 2));
2095
- process.exitCode = envStatus.webhook || envStatus.webhooks ? 0 : 1;
2281
+ console.log(JSON.stringify({ channel: notifyChannel, envStatus }, null, 2));
2282
+ process.exitCode = isNotifyTestEnvReady(envStatus, notifyChannel) ? 0 : 1;
2096
2283
  return;
2097
2284
  }
2098
2285
  if (args["print-message"] === "true") {
@@ -2101,7 +2288,17 @@ async function main() {
2101
2288
  return;
2102
2289
  }
2103
2290
  const attempts = await reviewAwareNotifier.dispatch(bundle);
2104
- console.log(JSON.stringify({ envStatus, message, attempts }, null, 2));
2291
+ console.log(JSON.stringify({
2292
+ channel: notifyChannel,
2293
+ envStatus,
2294
+ format: effectiveFormat,
2295
+ ...(notifyChannel === "app" ? {
2296
+ receiver: bundle.summary.qAssignee,
2297
+ appResolution
2298
+ } : {}),
2299
+ message,
2300
+ attempts
2301
+ }, null, 2));
2105
2302
  process.exitCode = attempts.every((attempt) => attempt.status === "dispatched") ? 0 : 1;
2106
2303
  return;
2107
2304
  }
@@ -2176,6 +2373,7 @@ async function main() {
2176
2373
  const existingBundle = await store.getLatestTaskDeliveryBundle(taskId);
2177
2374
  const latestRun = (await store.listTaskRuns()).filter((run) => run.taskId === taskId).at(-1) ?? null;
2178
2375
  const replayContext = await buildReplayExecutionContext({
2376
+ taskId,
2179
2377
  config,
2180
2378
  taskType: task.taskType,
2181
2379
  taskRepo: taskPackage.repo,