@sireai/optimus 0.1.45 → 0.1.46

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 (92) hide show
  1. package/dist/cli/feedback-delivery.js +13 -8
  2. package/dist/cli/feedback-delivery.js.map +1 -1
  3. package/dist/cli/optimus.js +370 -63
  4. package/dist/cli/optimus.js.map +1 -1
  5. package/dist/integrations/feishu/feishu-client.d.ts +13 -0
  6. package/dist/integrations/feishu/feishu-client.js +111 -6
  7. package/dist/integrations/feishu/feishu-client.js.map +1 -1
  8. package/dist/integrations/feishu/feishu-reference-material-downloader.d.ts +34 -0
  9. package/dist/integrations/feishu/feishu-reference-material-downloader.js +572 -0
  10. package/dist/integrations/feishu/feishu-reference-material-downloader.js.map +1 -0
  11. package/dist/integrations/feishu/feishu-token-store.d.ts +25 -1
  12. package/dist/integrations/feishu/feishu-token-store.js +148 -4
  13. package/dist/integrations/feishu/feishu-token-store.js.map +1 -1
  14. package/dist/integrations/feishu/feishu-user-auth-service.d.ts +39 -0
  15. package/dist/integrations/feishu/feishu-user-auth-service.js +228 -0
  16. package/dist/integrations/feishu/feishu-user-auth-service.js.map +1 -0
  17. package/dist/integrations/jira/jira-cli.js +127 -19
  18. package/dist/integrations/jira/jira-cli.js.map +1 -1
  19. package/dist/task-environment/delivery/commit-message/coder-commit-message-template.d.ts +12 -0
  20. package/dist/task-environment/delivery/commit-message/coder-commit-message-template.js +105 -0
  21. package/dist/task-environment/delivery/commit-message/coder-commit-message-template.js.map +1 -0
  22. package/dist/task-environment/delivery/commit-message/commit-message-builder.js +2 -1
  23. package/dist/task-environment/delivery/commit-message/commit-message-builder.js.map +1 -1
  24. package/dist/task-environment/delivery/feishu-analysis-doc-service.d.ts +6 -0
  25. package/dist/task-environment/delivery/feishu-analysis-doc-service.js +106 -34
  26. package/dist/task-environment/delivery/feishu-analysis-doc-service.js.map +1 -1
  27. package/dist/task-environment/delivery/feishu-content/feishu-content-renderer.js +16 -1
  28. package/dist/task-environment/delivery/feishu-content/feishu-content-renderer.js.map +1 -1
  29. package/dist/task-environment/delivery/feishu-content/feishu-copy-config.d.ts +6 -0
  30. package/dist/task-environment/delivery/feishu-content/feishu-copy-config.js +19 -0
  31. package/dist/task-environment/delivery/feishu-content/feishu-copy-config.js.map +1 -1
  32. package/dist/task-environment/delivery/feishu-notifier.js +8 -8
  33. package/dist/task-environment/delivery/feishu-notifier.js.map +1 -1
  34. package/dist/task-environment/delivery/feishu-templates/coder-message-template.d.ts +6 -0
  35. package/dist/task-environment/delivery/feishu-templates/coder-message-template.js +58 -0
  36. package/dist/task-environment/delivery/feishu-templates/coder-message-template.js.map +1 -0
  37. package/dist/task-environment/delivery/feishu-templates/template-registry.js +2 -0
  38. package/dist/task-environment/delivery/feishu-templates/template-registry.js.map +1 -1
  39. package/dist/task-environment/delivery/task-delivery-dispatcher.js +6 -0
  40. package/dist/task-environment/delivery/task-delivery-dispatcher.js.map +1 -1
  41. package/dist/task-environment/delivery/task-delivery-service.d.ts +1 -0
  42. package/dist/task-environment/delivery/task-delivery-service.js +124 -8
  43. package/dist/task-environment/delivery/task-delivery-service.js.map +1 -1
  44. package/dist/task-environment/delivery/task-publication-service.js +9 -6
  45. package/dist/task-environment/delivery/task-publication-service.js.map +1 -1
  46. package/dist/task-environment/document-input/document-structure.d.ts +13 -0
  47. package/dist/task-environment/document-input/document-structure.js +438 -0
  48. package/dist/task-environment/document-input/document-structure.js.map +1 -0
  49. package/dist/task-environment/intake/manual-problem-intake.js +36 -0
  50. package/dist/task-environment/intake/manual-problem-intake.js.map +1 -1
  51. package/dist/task-environment/observability/logger.d.ts +1 -0
  52. package/dist/task-environment/observability/logger.js +26 -0
  53. package/dist/task-environment/observability/logger.js.map +1 -1
  54. package/dist/task-environment/observability/runtime-panel.js +10 -1
  55. package/dist/task-environment/observability/runtime-panel.js.map +1 -1
  56. package/dist/task-environment/orchestration/reference-material-relocator.d.ts +2 -0
  57. package/dist/task-environment/orchestration/reference-material-relocator.js +69 -0
  58. package/dist/task-environment/orchestration/reference-material-relocator.js.map +1 -0
  59. package/dist/task-environment/orchestration/task-orchestrator.d.ts +3 -0
  60. package/dist/task-environment/orchestration/task-orchestrator.js +182 -8
  61. package/dist/task-environment/orchestration/task-orchestrator.js.map +1 -1
  62. package/dist/task-environment/orchestration/task-package-inputs.js +7 -1
  63. package/dist/task-environment/orchestration/task-package-inputs.js.map +1 -1
  64. package/dist/task-environment/orchestration/task-runtime-policy.js +11 -0
  65. package/dist/task-environment/orchestration/task-runtime-policy.js.map +1 -1
  66. package/dist/task-environment/orchestration/triage-runner.js +3 -0
  67. package/dist/task-environment/orchestration/triage-runner.js.map +1 -1
  68. package/dist/task-environment/runtime/optimus-runtime.js +3 -0
  69. package/dist/task-environment/runtime/optimus-runtime.js.map +1 -1
  70. package/dist/task-environment/storage/sqlite-task-store.d.ts +4 -0
  71. package/dist/task-environment/storage/sqlite-task-store.js +70 -1
  72. package/dist/task-environment/storage/sqlite-task-store.js.map +1 -1
  73. package/dist/task-environment/task-handler-descriptor.d.ts +5 -0
  74. package/dist/task-environment/task-handler-descriptor.js +15 -0
  75. package/dist/task-environment/task-handler-descriptor.js.map +1 -0
  76. package/dist/types.d.ts +47 -1
  77. package/embedded-skills/shared/feishu-task-inputs/SKILL.md +56 -0
  78. package/embedded-skills/shared/feishu-task-inputs/scripts/fetch-feishu-doc.mjs +756 -0
  79. package/embedded-skills/shared/feishu-task-inputs/skill.json +5 -0
  80. package/package.json +4 -1
  81. package/task-harnesses/coder/ACCEPT.md +73 -0
  82. package/task-harnesses/coder/CONSTRAINTS.md +72 -0
  83. package/task-harnesses/coder/CONTEXT.md +36 -0
  84. package/task-harnesses/coder/EVOLUTION.md +83 -0
  85. package/task-harnesses/coder/ROLE.md +39 -0
  86. package/task-harnesses/coder/STANDARD.md +258 -0
  87. package/task-harnesses/coder/manifest.json +13 -0
  88. package/task-harnesses/pm/ACCEPT.md +7 -0
  89. package/task-harnesses/pm/CONSTRAINTS.md +5 -0
  90. package/task-harnesses/pm/ROLE.md +5 -4
  91. package/task-harnesses/pm/STANDARD.md +31 -1
  92. package/task-harnesses/registry.json +4 -0
@@ -29,13 +29,16 @@ import { TaskDeliveryService } from "../task-environment/delivery/task-delivery-
29
29
  import { formatDeliveryWarningsWithDescriptions } from "../task-environment/delivery/delivery-warning-copy.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 { FeishuClient } from "../integrations/feishu/feishu-client.js";
32
+ import { FeishuClient, FeishuIntegrationError } from "../integrations/feishu/feishu-client.js";
33
33
  import { FeishuDocumentReader, mergeFeishuReferenceMaterials } from "../integrations/feishu/feishu-document-reader.js";
34
+ import { FeishuReferenceMaterialDownloader, isFeishuManagedReferenceMaterial } from "../integrations/feishu/feishu-reference-material-downloader.js";
35
+ import { FeishuUserAuthService } from "../integrations/feishu/feishu-user-auth-service.js";
34
36
  import { FeishuUserService } from "../integrations/feishu/feishu-user-service.js";
35
37
  import { createFeedbackReport } from "./feedback.js";
36
38
  import { DEFAULT_FEEDBACK_RECIPIENT_EMAIL, FeishuFeedbackDeliveryService, persistFeedbackDeliveryResult, resolveDefaultFeedbackRecipientFallbackOpenId } from "./feedback-delivery.js";
37
39
  import { resolveDefaultConfigPath, resolveDefaultEnvPath } from "../config/optimus-paths.js";
38
40
  import { checkForSelfUpdate, installSelfUpdate, recordSkippedSelfUpdate } from "./self-update.js";
41
+ import { buildDocumentStructure } from "../task-environment/document-input/document-structure.js";
39
42
  const CLI_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
40
43
  const PACKAGE_JSON_PATH = join(CLI_ROOT_DIR, "package.json");
41
44
  const execFileAsync = promisify(execFile);
@@ -43,6 +46,35 @@ const PACKAGE_NAME = "@sireai/optimus";
43
46
  const JIRA_CLI_ENTRY = join(CLI_ROOT_DIR, "dist", "integrations", "jira", "jira-cli.js");
44
47
  const SENTRY_CLI_ENTRY = join(CLI_ROOT_DIR, "dist", "integrations", "sentry", "sentry-cli.js");
45
48
  const RELEASE_NOTES_URL = "https://github.com/SireAI/optimus/releases/latest";
49
+ function createSilentSubmitConsoleSink() {
50
+ return () => {
51
+ // Submit preprocessing logs are still written to runtime logs; the interactive CLI keeps only loading + final result.
52
+ };
53
+ }
54
+ function resolveSubmitTaskType(input) {
55
+ const explicitTaskType = input.explicitTaskType?.trim();
56
+ if (explicitTaskType) {
57
+ return explicitTaskType;
58
+ }
59
+ if (looksLikePrototypeInput(input)) {
60
+ return "coder";
61
+ }
62
+ return "bugfix";
63
+ }
64
+ function looksLikePrototypeInput(input) {
65
+ const normalizedFileName = basename(input.filePath?.trim() ?? "").toLowerCase();
66
+ if (normalizedFileName === "prototype.html" || normalizedFileName === "prototype.htm") {
67
+ return true;
68
+ }
69
+ return (input.referenceMaterials ?? []).some((material) => isPrototypeReferenceMaterial(material));
70
+ }
71
+ function isPrototypeReferenceMaterial(material) {
72
+ if (material.type !== "file") {
73
+ return false;
74
+ }
75
+ const normalizedTitle = material.title.trim().toLowerCase();
76
+ return normalizedTitle === "prototype.html" || normalizedTitle === "prototype.htm";
77
+ }
46
78
  const DOCTOR_ANSI = {
47
79
  reset: "\u001B[0m",
48
80
  green: "\u001B[32m",
@@ -181,6 +213,61 @@ async function runWithTerminalLoading(message, action) {
181
213
  throw error;
182
214
  }
183
215
  }
216
+ async function runFeishuUserAuth(subcommand, commandArgs) {
217
+ const action = (subcommand?.trim() || "auth").toLowerCase();
218
+ const args = parseArgs(commandArgs);
219
+ const config = await loadConfig().catch(() => buildDefaultConfig());
220
+ const authService = new FeishuUserAuthService({ config });
221
+ if (action === "status") {
222
+ const user = await authService.readAuthorizedUser();
223
+ if (!user) {
224
+ console.log("Feishu user auth: not authorized");
225
+ console.log("Run `optimus feishu auth` to authorize your user account.");
226
+ return 1;
227
+ }
228
+ console.log("Feishu user auth: authorized");
229
+ if (user.openId) {
230
+ console.log(`Open ID: ${user.openId}`);
231
+ }
232
+ if (user.userId) {
233
+ console.log(`User ID: ${user.userId}`);
234
+ }
235
+ if (user.source) {
236
+ console.log(`Source: ${user.source}`);
237
+ }
238
+ console.log(`Expires: ${new Date(user.expireAtMs).toISOString()}`);
239
+ return 0;
240
+ }
241
+ if (action === "logout") {
242
+ await authService.clearAuthorizedUser();
243
+ console.log("Feishu user authorization cleared.");
244
+ return 0;
245
+ }
246
+ if (action !== "auth") {
247
+ console.error(renderCommandHelp("feishu") ?? "Unknown `optimus feishu` subcommand.");
248
+ return 1;
249
+ }
250
+ let announcedUrl = "";
251
+ const result = await authService.loginDeviceCode({
252
+ openBrowser: args["no-open"] !== "true",
253
+ onVerificationUri: (url, expiresIn) => {
254
+ announcedUrl = url;
255
+ console.log(`Open this URL to authorize Feishu access (expires in ${expiresIn}s):`);
256
+ console.log(url);
257
+ console.log("");
258
+ console.log("Waiting for authorization...");
259
+ }
260
+ });
261
+ if (!announcedUrl) {
262
+ console.log("Waiting for authorization...");
263
+ }
264
+ console.log("Feishu user authorization succeeded.");
265
+ console.log(`Authorization URL: ${result.verificationUriComplete}`);
266
+ if (result.openId) {
267
+ console.log(`Open ID: ${result.openId}`);
268
+ }
269
+ return 0;
270
+ }
184
271
  function renderDoctorSection(title, section, extraLines = []) {
185
272
  const lines = [formatDoctorStatusHeading(title, section.status)];
186
273
  if (section.reason) {
@@ -261,6 +348,7 @@ function renderCliHelp() {
261
348
  "",
262
349
  "Common commands:",
263
350
  " submit Submit a problem for Optimus to handle",
351
+ " feishu auth Authorize your Feishu user account for protected docs/wiki",
264
352
  " jira-submit-issue Submit a Jira issue into Optimus",
265
353
  " sentry-submit-event Submit a Sentry event into Optimus",
266
354
  " repo list Show registered repositories",
@@ -294,6 +382,7 @@ function renderAdvancedCliHelp() {
294
382
  " version Show the installed package version",
295
383
  "",
296
384
  "Advanced commands:",
385
+ " feishu auth|status|logout",
297
386
  " delivery-status Show focused delivery/publication state",
298
387
  " notify-test Send or preview a Feishu test notification",
299
388
  " delivery-retry Replay notification delivery from persisted bundle data",
@@ -462,6 +551,22 @@ function renderCommandHelp(command) {
462
551
  "Optional:",
463
552
  " --json Print JSON output for automation"
464
553
  ].join("\n"),
554
+ feishu: [
555
+ "optimus feishu",
556
+ "",
557
+ "Usage:",
558
+ " optimus feishu auth [options]",
559
+ " optimus feishu status",
560
+ " optimus feishu logout",
561
+ "",
562
+ "Subcommands:",
563
+ " auth Start Feishu user authorization via device code flow",
564
+ " status Show whether a Feishu user token is stored",
565
+ " logout Clear the stored Feishu user token",
566
+ "",
567
+ "Options for `auth`:",
568
+ " --no-open Print the authorization URL without opening a browser"
569
+ ].join("\n"),
465
570
  advanced: renderAdvancedCliHelp(),
466
571
  "notify-test": [
467
572
  "optimus notify-test",
@@ -2346,9 +2451,14 @@ function isFeishuDocumentUrl(value) {
2346
2451
  || /^https?:\/\/(?:[\w-]+\.)?larksuite\.com\//iu.test(value.trim());
2347
2452
  }
2348
2453
  function extractTitleFromMarkdown(markdown) {
2349
- const match = markdown.match(/<title>([\s\S]*?)<\/title>/iu);
2350
- const title = match?.[1]?.trim();
2351
- return title ? title : undefined;
2454
+ const htmlTitleMatch = markdown.match(/<title>([\s\S]*?)<\/title>/iu);
2455
+ const htmlTitle = htmlTitleMatch?.[1]?.trim();
2456
+ if (htmlTitle) {
2457
+ return htmlTitle;
2458
+ }
2459
+ const markdownHeadingMatch = markdown.match(/^\s*#\s+(.+?)\s*$/mu);
2460
+ const markdownHeading = markdownHeadingMatch?.[1]?.trim();
2461
+ return markdownHeading ? markdownHeading : undefined;
2352
2462
  }
2353
2463
  function parseHtmlAttributeMap(fragment) {
2354
2464
  const attributes = {};
@@ -2397,6 +2507,7 @@ function extractFeishuReferenceMaterials(markdown, media, sourceUrl) {
2397
2507
  const record = entry;
2398
2508
  const token = typeof record.token === "string" ? record.token.trim() : "";
2399
2509
  const type = typeof record.type === "string" ? record.type.trim() : "";
2510
+ const name = typeof record.name === "string" ? record.name.trim() : "";
2400
2511
  if (!token || !type) {
2401
2512
  continue;
2402
2513
  }
@@ -2413,7 +2524,7 @@ function extractFeishuReferenceMaterials(markdown, media, sourceUrl) {
2413
2524
  embeddedFileCount += 1;
2414
2525
  pushReference({
2415
2526
  type: "file",
2416
- title: `Embedded ${type} ${embeddedFileCount}`,
2527
+ title: name || `Embedded ${type} ${embeddedFileCount}`,
2417
2528
  token,
2418
2529
  sourceType: "feishu_media"
2419
2530
  });
@@ -2440,9 +2551,11 @@ function extractFeishuReferenceMaterials(markdown, media, sourceUrl) {
2440
2551
  while ((htmlImageMatch = htmlImagePattern.exec(markdown)) !== null) {
2441
2552
  embeddedImageCount += 1;
2442
2553
  const attrs = parseHtmlAttributeMap(htmlImageMatch[1] ?? "");
2554
+ const title = selectFeishuImageTitle(attrs, embeddedImageCount);
2443
2555
  pushReference({
2444
2556
  type: "image",
2445
- title: attrs.name?.trim() || `Embedded image ${embeddedImageCount}`,
2557
+ title,
2558
+ ...(attrs.href?.trim() ? { url: attrs.href.trim() } : {}),
2446
2559
  ...(attrs.src?.trim() ? { token: attrs.src.trim() } : {}),
2447
2560
  ...(attrs.mime?.trim() ? { mimeType: attrs.mime.trim() } : {}),
2448
2561
  sourceType: "feishu_markdown"
@@ -2541,7 +2654,18 @@ function extractFeishuReferenceMaterials(markdown, media, sourceUrl) {
2541
2654
  }
2542
2655
  return references.length > 0 ? references : undefined;
2543
2656
  }
2544
- async function resolveTaskTextFromUrl(url) {
2657
+ function selectFeishuImageTitle(attributes, ordinal) {
2658
+ const alt = attributes.alt?.trim();
2659
+ if (alt) {
2660
+ return alt;
2661
+ }
2662
+ const name = attributes.name?.trim();
2663
+ if (name) {
2664
+ return name;
2665
+ }
2666
+ return `Embedded image ${ordinal}`;
2667
+ }
2668
+ async function resolveTaskTextFromUrl(url, config, logger) {
2545
2669
  const trimmedUrl = url.trim();
2546
2670
  if (!trimmedUrl) {
2547
2671
  throw new Error("Task document URL is empty.");
@@ -2549,60 +2673,228 @@ async function resolveTaskTextFromUrl(url) {
2549
2673
  if (!isFeishuDocumentUrl(trimmedUrl)) {
2550
2674
  throw new Error("submit currently supports document URLs from Feishu/Lark only.");
2551
2675
  }
2552
- const reader = new FeishuDocumentReader();
2553
2676
  try {
2554
- const resolved = await reader.readFromUrl(trimmedUrl);
2555
- const referenceMaterials = mergeFeishuReferenceMaterials(extractFeishuReferenceMaterials(resolved.content, undefined, trimmedUrl), resolved.referenceMaterials);
2556
- return {
2557
- content: resolved.content,
2558
- ...(resolved.title ? { title: resolved.title } : {}),
2559
- sourceType: resolved.sourceType,
2560
- ...(resolved.sourceDocumentType ? { sourceDocumentType: resolved.sourceDocumentType } : {}),
2561
- ...(referenceMaterials ? { referenceMaterials } : {})
2562
- };
2677
+ return await resolveTaskTextFromUrlWithAuthMode(trimmedUrl, "tenant", config, logger);
2563
2678
  }
2564
2679
  catch (builtinError) {
2565
- let stdout;
2566
- try {
2567
- const result = await execFileAsync("feishu", ["fetch", trimmedUrl], {
2568
- maxBuffer: 10 * 1024 * 1024
2569
- });
2570
- stdout = result.stdout;
2680
+ if (shouldRetryFeishuSubmitWithUserAuth(builtinError) && config) {
2681
+ const userAuthService = new FeishuUserAuthService({ config });
2682
+ const hadStoredUserAuth = await userAuthService.hasAuthorizedUser();
2683
+ if (hadStoredUserAuth) {
2684
+ try {
2685
+ return await resolveTaskTextFromUrlWithAuthMode(trimmedUrl, "user", config, logger);
2686
+ }
2687
+ catch (userError) {
2688
+ if (shouldRetryFeishuSubmitWithFreshUserAuth(userError)) {
2689
+ try {
2690
+ const adopted = await userAuthService.adoptLegacyCliSession();
2691
+ if (adopted) {
2692
+ return await resolveTaskTextFromUrlWithAuthMode(trimmedUrl, "user", config, logger);
2693
+ }
2694
+ }
2695
+ catch {
2696
+ // Ignore legacy session adoption failures and continue to interactive re-authorization.
2697
+ }
2698
+ try {
2699
+ await startFeishuUserAuthForSubmit(userAuthService);
2700
+ return await resolveTaskTextFromUrlWithAuthMode(trimmedUrl, "user", config, logger);
2701
+ }
2702
+ catch (reauthError) {
2703
+ const appDetail = formatFeishuSubmitError(builtinError);
2704
+ const userDetail = formatFeishuSubmitError(userError);
2705
+ const reauthDetail = reauthError instanceof Error ? reauthError.message : String(reauthError);
2706
+ throw new Error(`submit could not fetch the Feishu document via the built-in Feishu app (${appDetail}). `
2707
+ + `Stored user authorization was insufficient (${userDetail}). `
2708
+ + `Automatic user re-authorization also failed (${reauthDetail}).`);
2709
+ }
2710
+ }
2711
+ const appDetail = formatFeishuSubmitError(builtinError);
2712
+ const userDetail = formatFeishuSubmitError(userError);
2713
+ throw new Error(`submit could not fetch the Feishu document via the built-in Feishu app (${appDetail}). User-auth fallback also failed (${userDetail}).`);
2714
+ }
2715
+ }
2716
+ const builtinDetail = formatFeishuSubmitError(builtinError);
2717
+ try {
2718
+ await startFeishuUserAuthForSubmit(userAuthService);
2719
+ try {
2720
+ return await resolveTaskTextFromUrlWithAuthMode(trimmedUrl, "user", config, logger);
2721
+ }
2722
+ catch (freshUserError) {
2723
+ if (shouldRetryFeishuSubmitWithFreshUserAuth(freshUserError)) {
2724
+ const scopeDetail = formatFeishuSubmitError(freshUserError);
2725
+ throw new Error(`Feishu user authorization succeeded, but the authorized Optimus Feishu app still lacks the required user privileges (${scopeDetail}). `
2726
+ + "Open the app's user scopes, publish the updated Feishu app version, then authorize again with `optimus feishu auth`.");
2727
+ }
2728
+ throw freshUserError;
2729
+ }
2730
+ }
2731
+ catch (authError) {
2732
+ const authDetail = authError instanceof Error ? authError.message : String(authError);
2733
+ throw new Error(`submit could not fetch the Feishu document via the built-in Feishu app (${builtinDetail}). `
2734
+ + `Automatic Feishu user authorization failed (${authDetail}).`);
2735
+ }
2571
2736
  }
2572
- catch (error) {
2573
- const execError = error;
2574
- const cliDetail = execError?.code === "ENOENT"
2575
- ? "the `feishu` CLI is not installed or not on PATH"
2576
- : execError?.stderr?.trim() || execError?.stdout?.trim() || execError?.message || "unknown error";
2577
- const builtinDetail = builtinError instanceof Error ? builtinError.message : String(builtinError);
2578
- throw new Error(`submit could not fetch the Feishu document via built-in app (${builtinDetail}) or via \`feishu fetch\` (${cliDetail}).`);
2579
- }
2580
- let payload;
2581
- try {
2582
- payload = JSON.parse(stdout);
2737
+ const builtinDetail = formatFeishuSubmitError(builtinError);
2738
+ throw new Error(`submit could not fetch the Feishu document via the built-in Feishu app (${builtinDetail}).`);
2739
+ }
2740
+ }
2741
+ async function startFeishuUserAuthForSubmit(authService) {
2742
+ let announcedUrl = "";
2743
+ console.error("Feishu document access requires user authorization. Starting authorization flow...");
2744
+ await authService.loginDeviceCode({
2745
+ onVerificationUri: (url, expiresIn) => {
2746
+ announcedUrl = url;
2747
+ console.error(`Authorize Feishu access in your browser within ${expiresIn}s:`);
2748
+ console.error(url);
2749
+ console.error("Waiting for authorization...");
2583
2750
  }
2584
- catch {
2585
- throw new Error("submit received non-JSON output from `feishu fetch`.");
2751
+ });
2752
+ if (!announcedUrl) {
2753
+ console.error("Waiting for authorization...");
2754
+ }
2755
+ console.error("Feishu user authorization completed. Retrying document fetch...");
2756
+ }
2757
+ async function resolveTaskTextFromUrlWithAuthMode(trimmedUrl, authMode, config, logger) {
2758
+ const reader = new FeishuDocumentReader({
2759
+ ...(config ? { config } : {}),
2760
+ client: new FeishuClient({
2761
+ ...(config ? { config } : {}),
2762
+ authMode
2763
+ })
2764
+ });
2765
+ const resolved = await reader.readFromUrl(trimmedUrl);
2766
+ const resolvedTitle = resolved.title?.trim() || extractTitleFromMarkdown(resolved.content);
2767
+ const referenceMaterials = mergeFeishuReferenceMaterials(extractFeishuReferenceMaterials(resolved.content, undefined, trimmedUrl), resolved.referenceMaterials);
2768
+ await logger?.info("reference_materials.detected", {
2769
+ sourceUrl: trimmedUrl,
2770
+ sourceDocumentType: resolved.sourceDocumentType ?? null,
2771
+ materialCount: referenceMaterials?.length ?? 0,
2772
+ fileCount: referenceMaterials?.filter((entry) => entry.type === "file").length ?? 0,
2773
+ imageCount: referenceMaterials?.filter((entry) => entry.type === "image").length ?? 0,
2774
+ structuredObjectCount: referenceMaterials?.filter((entry) => !["file", "image", "link"].includes(entry.type)).length ?? 0,
2775
+ feishuAuthMode: authMode
2776
+ });
2777
+ const downloadedReferenceMaterials = config
2778
+ ? await new FeishuReferenceMaterialDownloader({
2779
+ config,
2780
+ client: new FeishuClient({
2781
+ config,
2782
+ authMode
2783
+ }),
2784
+ ...(logger ? { logger } : {})
2785
+ }).downloadReferenceMaterials(referenceMaterials ?? [], buildRemoteInputNamespace(trimmedUrl), trimmedUrl)
2786
+ : referenceMaterials;
2787
+ await ensureFeishuImageDownloadsSucceeded(downloadedReferenceMaterials, trimmedUrl, logger);
2788
+ const documentStructure = buildDocumentStructure(resolved.content, {
2789
+ ...(resolvedTitle ? { title: resolvedTitle } : {}),
2790
+ sourceUrl: trimmedUrl,
2791
+ ...(downloadedReferenceMaterials ? { referenceMaterials: downloadedReferenceMaterials } : {})
2792
+ });
2793
+ return {
2794
+ content: resolved.content,
2795
+ ...(resolvedTitle ? { title: resolvedTitle } : {}),
2796
+ sourceType: resolved.sourceType,
2797
+ ...(resolved.sourceDocumentType ? { sourceDocumentType: resolved.sourceDocumentType } : {}),
2798
+ ...(documentStructure.referenceMaterials && documentStructure.referenceMaterials.length > 0 ? { referenceMaterials: documentStructure.referenceMaterials } : {}),
2799
+ documentMap: documentStructure.documentMap,
2800
+ ...(documentStructure.evidenceBundles.length > 0 ? { evidenceBundles: documentStructure.evidenceBundles } : {})
2801
+ };
2802
+ }
2803
+ function formatFeishuSubmitError(error) {
2804
+ if (error instanceof FeishuIntegrationError) {
2805
+ const parts = [error.message];
2806
+ const status = Number.isFinite(error.diagnostics.status) ? String(error.diagnostics.status) : undefined;
2807
+ const code = Number.isFinite(error.diagnostics.code) ? String(error.diagnostics.code) : undefined;
2808
+ if (status) {
2809
+ parts.push(`status=${status}`);
2810
+ }
2811
+ if (code) {
2812
+ parts.push(`code=${code}`);
2813
+ }
2814
+ const detail = extractFeishuErrorDetail(error.diagnostics.detail);
2815
+ if (detail) {
2816
+ parts.push(detail);
2586
2817
  }
2587
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
2588
- throw new Error("submit received an invalid response from `feishu fetch`.");
2818
+ return parts.join(", ");
2819
+ }
2820
+ return error instanceof Error ? error.message : String(error);
2821
+ }
2822
+ function extractFeishuErrorDetail(detail) {
2823
+ const trimmed = detail?.trim();
2824
+ if (!trimmed) {
2825
+ return undefined;
2826
+ }
2827
+ try {
2828
+ const parsed = JSON.parse(trimmed);
2829
+ const candidates = [
2830
+ typeof parsed.msg === "string" ? parsed.msg.trim() : "",
2831
+ typeof parsed.error?.message === "string" ? parsed.error.message.trim() : ""
2832
+ ].filter((entry) => entry.length > 0);
2833
+ if (candidates.length > 0) {
2834
+ return candidates.join(" | ");
2589
2835
  }
2590
- const record = payload;
2591
- const markdown = typeof record.markdown === "string" ? record.markdown.trim() : "";
2592
- const title = (typeof record.title === "string" ? record.title.trim() : "") || extractTitleFromMarkdown(markdown);
2593
- const sourceDocumentType = typeof record.type === "string" ? record.type.trim() : undefined;
2594
- const referenceMaterials = extractFeishuReferenceMaterials(markdown, record.media, trimmedUrl);
2595
- if (!markdown) {
2596
- throw new Error("submit fetched the document URL, but it did not return readable markdown content.");
2836
+ }
2837
+ catch {
2838
+ return trimmed;
2839
+ }
2840
+ return trimmed;
2841
+ }
2842
+ function shouldRetryFeishuSubmitWithUserAuth(error) {
2843
+ if (!(error instanceof FeishuIntegrationError)) {
2844
+ return false;
2845
+ }
2846
+ if (error.diagnostics.code === 131006) {
2847
+ return true;
2848
+ }
2849
+ const detail = error.diagnostics.detail?.toLowerCase() ?? "";
2850
+ return /permission denied|tenant needs read permission|node permission denied/.test(detail);
2851
+ }
2852
+ function shouldRetryFeishuSubmitWithFreshUserAuth(error) {
2853
+ if (error instanceof FeishuIntegrationError) {
2854
+ if (error.diagnostics.code === 99991679) {
2855
+ return true;
2597
2856
  }
2598
- return {
2599
- content: markdown,
2600
- ...(title ? { title } : {}),
2601
- sourceType: "feishu_doc",
2602
- ...(sourceDocumentType ? { sourceDocumentType } : {}),
2603
- ...(referenceMaterials ? { referenceMaterials } : {})
2604
- };
2857
+ const detail = error.diagnostics.detail?.toLowerCase() ?? "";
2858
+ return /request user re-authorization|user re-authorization|docs:document\.media:download|unauthorized/.test(detail);
2859
+ }
2860
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
2861
+ return /99991679|request user re-authorization|user re-authorization|docs:document\.media:download/.test(message);
2862
+ }
2863
+ function buildRemoteInputNamespace(sourceUrl) {
2864
+ const compactTime = new Date().toISOString().replace(/[-:TZ.]/gu, "").slice(0, 14);
2865
+ const token = sourceUrl.match(/\/(?:docx|wiki)\/([A-Za-z0-9]+)/u)?.[1]?.trim() || "remote-doc";
2866
+ return `${compactTime}-${token}`;
2867
+ }
2868
+ async function ensureFeishuImageDownloadsSucceeded(materials, sourceUrl, logger) {
2869
+ const images = (materials ?? []).filter((entry) => entry.type === "image" && isFeishuManagedReferenceMaterial(entry));
2870
+ if (images.length === 0) {
2871
+ return;
2605
2872
  }
2873
+ const missing = images.filter((entry) => !entry.localPath?.trim() || entry.downloadStatus !== "downloaded");
2874
+ if (missing.length === 0) {
2875
+ return;
2876
+ }
2877
+ const detail = missing
2878
+ .slice(0, 3)
2879
+ .map((entry) => {
2880
+ const title = entry.title?.trim() || "Untitled image";
2881
+ const reason = entry.downloadError?.trim()
2882
+ || (entry.downloadStatus === "skipped" ? "download was skipped" : "no local file was produced");
2883
+ return `${title} (${reason})`;
2884
+ })
2885
+ .join("; ");
2886
+ const remainder = missing.length > 3 ? `; and ${missing.length - 3} more` : "";
2887
+ await logger?.warn("reference_materials.image_download_blocked", {
2888
+ sourceUrl,
2889
+ imageCount: images.length,
2890
+ missingImageCount: missing.length,
2891
+ missingImageTitles: missing.map((entry) => entry.title),
2892
+ missingImageReasons: missing.map((entry) => entry.downloadError ?? entry.downloadStatus ?? "missing_local_path")
2893
+ });
2894
+ throw new Error(`submit could not prepare ${missing.length}/${images.length} embedded image(s) from the Feishu document. `
2895
+ + `Embedded screenshots must be downloaded before the task starts. `
2896
+ + `Failed image(s): ${detail}${remainder}. `
2897
+ + "Check Feishu document image permissions and retry.");
2606
2898
  }
2607
2899
  async function main() {
2608
2900
  const argv = process.argv.slice(2);
@@ -2707,8 +2999,7 @@ async function main() {
2707
2999
  return;
2708
3000
  }
2709
3001
  if (command === "feishu") {
2710
- console.log("Feishu user-session commands have been removed. Optimus now uses the built-in Feishu app for delivery and document publishing.");
2711
- process.exitCode = 1;
3002
+ process.exitCode = await runFeishuUserAuth(commandArgs[0], commandArgs.slice(1));
2712
3003
  return;
2713
3004
  }
2714
3005
  if (mapJiraCommand(command)) {
@@ -2795,7 +3086,6 @@ async function main() {
2795
3086
  if (command === "submit") {
2796
3087
  const args = parseArgs(commandArgs);
2797
3088
  const explicitTitle = args.title?.trim();
2798
- const taskType = args["task-type"]?.trim() || "bugfix";
2799
3089
  const inlineDescription = args.desc?.trim() || args.description?.trim();
2800
3090
  const filePath = args.file?.trim();
2801
3091
  const url = args.url?.trim();
@@ -2811,6 +3101,8 @@ async function main() {
2811
3101
  let remoteDocumentTitle;
2812
3102
  let remoteDocumentType;
2813
3103
  let referenceMaterials;
3104
+ let documentMap;
3105
+ let evidenceBundles;
2814
3106
  if (inlineDescription) {
2815
3107
  description = inlineDescription;
2816
3108
  }
@@ -2819,13 +3111,15 @@ async function main() {
2819
3111
  }
2820
3112
  else {
2821
3113
  try {
2822
- const remote = await runWithTerminalLoading("Resolving remote requirement document", async () => await resolveTaskTextFromUrl(url));
3114
+ const remote = await OptimusLogger.withConsoleSink(createSilentSubmitConsoleSink(), async () => await runWithTerminalLoading("Resolving remote requirement document", async () => await resolveTaskTextFromUrl(url, config, logger)));
2823
3115
  description = remote.content;
2824
3116
  resolvedTitle = resolvedTitle || remote.title;
2825
3117
  remoteSourceType = remote.sourceType;
2826
3118
  remoteDocumentTitle = remote.title;
2827
3119
  remoteDocumentType = remote.sourceDocumentType;
2828
3120
  referenceMaterials = remote.referenceMaterials;
3121
+ documentMap = remote.documentMap;
3122
+ evidenceBundles = remote.evidenceBundles;
2829
3123
  }
2830
3124
  catch (error) {
2831
3125
  console.error(error instanceof Error ? error.message : String(error));
@@ -2833,36 +3127,49 @@ async function main() {
2833
3127
  return;
2834
3128
  }
2835
3129
  }
2836
- if (!resolvedTitle || !description) {
2837
- console.error(renderShellQuotingHint("submit"));
3130
+ const taskType = resolveSubmitTaskType({
3131
+ ...(args["task-type"]?.trim() ? { explicitTaskType: args["task-type"].trim() } : {}),
3132
+ ...(filePath ? { filePath } : {}),
3133
+ ...(referenceMaterials ? { referenceMaterials } : {})
3134
+ });
3135
+ if (!description) {
3136
+ console.error(url
3137
+ ? "submit resolved the remote document, but it did not produce readable content."
3138
+ : renderShellQuotingHint("submit"));
2838
3139
  process.exitCode = 1;
2839
3140
  return;
2840
3141
  }
2841
- if (!description) {
2842
- console.error(renderShellQuotingHint("submit"));
3142
+ if (!resolvedTitle) {
3143
+ console.error(url
3144
+ ? "submit resolved the remote document content, but could not determine a title. Pass --title explicitly."
3145
+ : renderShellQuotingHint("submit"));
2843
3146
  process.exitCode = 1;
2844
3147
  return;
2845
3148
  }
2846
- await store.init();
2847
- const repositorySelection = await resolveSubmissionRepositorySelector(taskType, args, store, config);
3149
+ await runWithTerminalLoading("Checking submission workspace", async () => await store.init());
3150
+ const repositorySelection = await runWithTerminalLoading("Resolving submission target", async () => await resolveSubmissionRepositorySelector(taskType, args, store, config));
2848
3151
  if (!repositorySelection.ok) {
2849
3152
  console.error(repositorySelection.reason);
2850
3153
  process.exitCode = 1;
2851
3154
  return;
2852
3155
  }
3156
+ const shouldPersistTaskTypeHint = Boolean(args["task-type"]?.trim()) || taskType !== "bugfix";
2853
3157
  const event = createManualProblemEvent({
2854
3158
  ...args,
2855
3159
  title: resolvedTitle,
2856
3160
  description,
3161
+ ...(shouldPersistTaskTypeHint ? { "task-type": taskType } : {}),
2857
3162
  ...(url ? { sourceUrl: url } : {}),
2858
3163
  ...(remoteSourceType ? { "source-type": remoteSourceType } : {}),
2859
3164
  ...(remoteDocumentTitle ? { "source-document-title": remoteDocumentTitle } : {}),
2860
3165
  ...(remoteDocumentType ? { "source-document-type": remoteDocumentType } : {}),
2861
3166
  ...(referenceMaterials ? { "reference-materials-json": JSON.stringify(referenceMaterials) } : {}),
3167
+ ...(documentMap ? { "document-map-json": JSON.stringify(documentMap) } : {}),
3168
+ ...(evidenceBundles ? { "evidence-bundles-json": JSON.stringify(evidenceBundles) } : {}),
2862
3169
  ...(repositorySelection.repoSelector ? { repo: repositorySelection.repoSelector } : {}),
2863
3170
  ...(args["allow-duplicate"] === "true" ? { "allow-duplicate": "true" } : {})
2864
3171
  });
2865
- await runtime.writeManualSubmission(event);
3172
+ await runWithTerminalLoading("Submitting task", async () => await runtime.writeManualSubmission(event));
2866
3173
  console.log("[optimus] submit succeeded");
2867
3174
  return;
2868
3175
  }