@mytegroupinc/myte-core 0.0.47 → 0.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -7,9 +7,12 @@
7
7
  * - Deterministic diffs: fetch project config, resolve local project repos, then collect scoped git context
8
8
  */
9
9
 
10
- const fs = require("fs");
11
- const path = require("path");
12
- const { createHash } = require("crypto");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const dns = require("dns");
13
+ const net = require("net");
14
+ const tls = require("tls");
15
+ const { createHash, randomUUID } = require("crypto");
13
16
  const { spawnSync } = require("child_process");
14
17
  const {
15
18
  DEFAULT_MYTEAI_BASE,
@@ -21,8 +24,10 @@ const {
21
24
  normalizeMyteAiBase,
22
25
  } = require("./lib/ai-gateway");
23
26
 
24
- const DEFAULT_API_BASE = "https://api.myte.dev";
25
- const DEFAULT_DIFF_LIMIT_CHARS = 500_000;
27
+ const DEFAULT_API_BASE = "https://api.myte.dev";
28
+ const DEFAULT_DIFF_LIMIT_CHARS = 500_000;
29
+ const DEFAULT_FEEDBACK_SYNC_PAGE_SIZE = 50;
30
+ const MAX_PRD_DOCUMENT_MARKDOWN_CHARS = 250_000;
26
31
  const REMOVED_COMMAND_MESSAGES = {
27
32
  ask: "The `ask` alias has been removed. Use `myte query \"...\"`.",
28
33
  chat: "The `chat` command has been removed. Use repeated `myte query` calls instead.",
@@ -45,11 +50,11 @@ function findEnvPath(startDir) {
45
50
  return null;
46
51
  }
47
52
 
48
- function loadEnv() {
49
- const envPath = findEnvPath(process.cwd());
50
- if (!envPath || !fs.existsSync(envPath)) return;
53
+ function loadEnv() {
54
+ const envPath = findEnvPath(process.cwd());
55
+ if (!envPath || !fs.existsSync(envPath)) return null;
51
56
  const content = fs.readFileSync(envPath, "utf8");
52
- content.split(/\r?\n/).forEach((line) => {
57
+ content.split(/\r?\n/).forEach((line) => {
53
58
  const trimmed = String(line || "").trim();
54
59
  if (!trimmed || trimmed.startsWith("#")) return;
55
60
  const idx = trimmed.indexOf("=");
@@ -63,10 +68,11 @@ function loadEnv() {
63
68
  val = val.slice(1, -1);
64
69
  }
65
70
  if (key && !(key in process.env)) {
66
- process.env[key] = val;
67
- }
68
- });
69
- }
71
+ process.env[key] = val;
72
+ }
73
+ });
74
+ return envPath;
75
+ }
70
76
 
71
77
  function splitCommand(argv) {
72
78
  if (argv[0] === "mission") {
@@ -89,8 +95,13 @@ function splitCommand(argv) {
89
95
  "query",
90
96
  "ai",
91
97
  "ask",
92
- "chat",
93
- "config",
98
+ "chat",
99
+ "config",
100
+ "doctor",
101
+ "info",
102
+ "version",
103
+ "--version",
104
+ "-v",
94
105
  "bootstrap",
95
106
  "suggestions",
96
107
  "run-qaqc",
@@ -112,8 +123,13 @@ function splitCommand(argv) {
112
123
  "-h",
113
124
  ]);
114
125
  const first = argv[0];
115
- if (first && known.has(first)) {
116
- const cmd = first === "--help" || first === "-h" ? "help" : first;
126
+ if (first && known.has(first)) {
127
+ const cmd =
128
+ first === "--help" || first === "-h"
129
+ ? "help"
130
+ : first === "--version" || first === "-v"
131
+ ? "version"
132
+ : first;
117
133
  return { command: cmd, rest: argv.slice(1) };
118
134
  }
119
135
  return { command: "query", rest: argv };
@@ -181,9 +197,12 @@ function printHelp() {
181
197
  "myte - Myte CLI",
182
198
  "",
183
199
  "Usage:",
184
- " myte query \"<text>\" [--with-diff] [--context \"...\"]",
200
+ " myte query \"<text>\" [--with-diff] [--context \"...\"] [--request-id <id>] [--verbose] [--json]",
185
201
  " myte ai \"<text>\" [--json-response] [--max-output-tokens 500]",
186
- " myte config [--json]",
202
+ " myte config [--json]",
203
+ " myte doctor [--json] [--timeout-ms <ms>] [--base-url <url>]",
204
+ " myte info [--json]",
205
+ " myte --version [--verbose] [--json]",
187
206
  " myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
188
207
  " myte run-qaqc --mission-ids \"M001[,M002...]\" [--wait] [--sync] [--force] [--json]",
189
208
  " myte mission status --mission-ids \"M001[,M002...]\" --status todo|in_progress|done [--no-sync] [--json]",
@@ -200,23 +219,23 @@ function printHelp() {
200
219
  " myte feedback get --feedback-id <id> [--json]",
201
220
  " myte feedback history --feedback-id <id> [--limit 50] [--json]",
202
221
  " myte feedback status --feedback-id <id> --status frozen|todo|in_progress|in_review|completed|deployed|rejected|archived --reason \"...\"",
203
- " myte feedback edit --feedback-id <id> [--title \"...\"] [--feedback-text \"...\"] [--priority High] [--reason \"...\"]",
222
+ " myte feedback edit --feedback-id <id> [--title \"...\"] [--feedback-text \"...\"] [--prd-file ./document.md] [--document-id <id>] [--priority High] [--reason \"...\"]",
204
223
  " myte feedback assign --feedback-id <id> --user-id <id> [--reason \"...\"]",
205
224
  " myte feedback archive --feedback-id <id> --reason \"...\"",
206
225
  " myte feedback submit --file ./MyteCommandCenter/reviews/feedback/<id>-edit.yml --confirm-write --approval-artifact <path> [--json]",
207
226
  " myte feedback revise --request-id <id> --file ./MyteCommandCenter/reviews/feedback/<id>-edit.yml --confirm-write --approval-artifact <path> [--json]",
208
227
  " myte feedback reviews [--status open|terminal|all] [--request-id <id>] [--json]",
209
- " myte feedback review --request-id <id> --action approve|reject|request_changes|cancel [--reason \"...\"] [--json]",
210
- " myte feedback review --request-ids \"<id1,id2>\" --action approve|reject|request_changes|cancel [--reason \"...\"] [--json]",
228
+ " myte feedback review --request-id <id> --action approve|reject|request_changes|cancel [--reason \"...\"] [--verification-evidence \"...\"] [--deployment-evidence \"...\"] [--json]",
229
+ " myte feedback review --request-ids \"<id1,id2>\" --action approve|reject|request_changes|cancel [--reason \"...\"] [--verification-evidence \"...\"] [--deployment-evidence \"...\"] [--json]",
211
230
  " myte feedback move --feedback-id <id> --to-state in_progress --from-state todo --reason \"...\" [--json]",
212
231
  " myte feedback move --feedback-ids \"<id1,id2>\" --to-state in_progress --reason \"...\" [--json]",
213
232
  " myte feedback comment --feedback-id <id> --body \"...\" [--body-file ./comment.md] --confirm-write --approval-artifact <path> [--json]",
214
233
  " myte feedback undo --feedback-id <id> --event-id <id> --reason \"...\" [--json]",
215
234
  " myte feedback prd-versions --feedback-id <id> [--json]",
216
- " myte feedback prd-diff --feedback-id <id> --version-id <id> [--compare-to <id>] [--json]",
235
+ " myte feedback prd-diff --feedback-id <id> --version-id <id> [--compare-to <id>] [--document-id <id>] [--json]",
217
236
  " myte feedback validate --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
218
237
  " myte feedback apply --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
219
- " myte create-prd <file.md> [more.md ...] --confirm-write --approval-artifact <path> [--json] [--title \"...\"] [--description \"...\"]",
238
+ " myte create-prd <file.md> [more.md ...] [--document-set] --confirm-write --approval-artifact <path> [--json] [--title \"...\"] [--description \"...\"]",
220
239
  " cat file.md | myte create-prd --stdin --confirm-write --approval-artifact <path> [--title \"...\"] [--description \"...\"]",
221
240
  " cat update.md | myte update-owner --stdin --subject \"Owner update\" --confirm-write --approval-artifact ./update.md",
222
241
  " cat update.md | myte update-client --stdin --subject \"Weekly client update\" --confirm-write --approval-artifact ./update.md",
@@ -282,7 +301,9 @@ function printHelp() {
282
301
  "",
283
302
  "create-prd contract:",
284
303
  " - Required: valid MYTE_API_KEY, PRD markdown body, title",
285
- " - Accepts one file or many files per command; multi-file uploads are sent in one deterministic batch request",
304
+ " - Accepts one file or many files per command; multi-file uploads create separate feedback items by default",
305
+ " - Add --document-set to create one feedback item containing up to three ordered markdown documents",
306
+ " - Document-set source markdown is preserved exactly; canonical markdown is generated server-side for reading and DOCX rendering",
286
307
  " - Title source: myte-kanban.title, first # heading, or --title",
287
308
  " - Description source: myte-kanban.description or --description; this is a short feedback/card summary only",
288
309
  " - Never put the full PRD in description. The complete PRD must live in the markdown file body",
@@ -327,7 +348,8 @@ function printHelp() {
327
348
  " - comment creates text-only feedback-specific comments; attachments remain web UI only for this endpoint",
328
349
  " - project-key API/CLI cannot unarchive archived Feedback; use the web archived Feedback view",
329
350
  " - get/history read current feedback snapshots and audited event history through the project-key API",
330
- " - prd-versions/prd-diff expose retained PRD versions and backend-generated text diffs",
351
+ " - prd-versions/prd-diff expose retained PRD versions and backend-generated per-document text diffs",
352
+ " - edit/refine with --prd-file updates a review artifact only; --document-id targets one document in a PRD document set",
331
353
  " - validate/apply send artifacts to /api/project-assistant/feedback/<id>/refinement/* for validation or owner-direct apply",
332
354
  " - The backend owns authorization, stale snapshot checks, allowed field/transition rules, and history",
333
355
  " - apply is idempotent and does not rewrite local feedback.yml; run feedback-sync after apply to refresh local state",
@@ -364,7 +386,9 @@ function printHelp() {
364
386
  "Options:",
365
387
  " --with-diff Include deterministic git diffs (project-scoped; fails fast if no project repos are configured or resolved)",
366
388
  " --diff-limit <chars> Truncate diff context to N chars (default: 500000)",
367
- " --timeout-ms <ms> Request timeout (default: 300000)",
389
+ " --timeout-ms <ms> Request timeout (default: 300000)",
390
+ " --verbose Print sanitized transport and retry diagnostics",
391
+ " --request-id <id> Stable, non-secret query request/idempotency identifier",
368
392
  " --base-url <url> API base (default: https://api.myte.dev)",
369
393
  " --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
370
394
  " --json-response Ask the Myte AI gateway to return clean JSON only and send OpenAI-compatible response_format",
@@ -373,8 +397,9 @@ function printHelp() {
373
397
  " --output-dir <path> Command Center output directory (default: <current-workspace>/MyteCommandCenter)",
374
398
  " --file <path> YAML/JSON payload file for suggestions create/revise/review",
375
399
  " --stdin Read supported command content from stdin instead of inline text or a file path",
376
- " --title <text> Override PRD title for raw markdown uploads",
377
- " --description <text> Set feedback description/card summary for raw markdown uploads",
400
+ " --title <text> Override PRD title for raw markdown uploads",
401
+ " --description <text> Set feedback description/card summary for raw markdown uploads",
402
+ " --document-set Create one PRD feedback item containing one to three ordered markdown files",
378
403
  " --content <text> Team update content for update-team",
379
404
  " --body <text> Feedback comment body for feedback comment",
380
405
  " --subject <text> Subject for update-owner or update-client",
@@ -387,8 +412,10 @@ function printHelp() {
387
412
  " --request-id <id> Feedback review request ObjectId for submit/revise/review flows",
388
413
  " --request-ids <ids> Comma-separated Feedback review request ObjectIds for batch review",
389
414
  " --event-id <id> Feedback board event ObjectId for undo",
390
- " --version-id <id> Feedback PRD version ObjectId for prd-diff",
391
- " --compare-to <id> Optional base PRD version ObjectId for prd-diff",
415
+ " --version-id <id> Feedback PRD version ObjectId for prd-diff",
416
+ " --compare-to <id> Optional base PRD version ObjectId for prd-diff",
417
+ " --document-id <id> Stable PRD document id for document refinement or version diff",
418
+ " --prd-file <path> Read proposed PRD document Markdown into a local feedback review artifact",
392
419
  " --action <value> Feedback review action: approve, reject, request_changes, or cancel",
393
420
  " --to-state <value> Target canonical feedback board state for feedback move",
394
421
  " --from-state <value> Optional current-state guard for feedback move",
@@ -415,7 +442,10 @@ function printHelp() {
415
442
  " --no-fetch Don't git fetch origin main/master before diff",
416
443
  "",
417
444
  "Examples:",
418
- " myte query \"What changed in logging?\" --with-diff",
445
+ " myte query \"What changed in logging?\" --with-diff",
446
+ " myte doctor --json",
447
+ " myte info --json",
448
+ " myte --version --verbose",
419
449
  " myte ai \"Explain what this repository does\"",
420
450
  " myte ai \"Return a JSON object with risks and next_steps\" --json-response",
421
451
  " myte bootstrap",
@@ -446,6 +476,7 @@ function printHelp() {
446
476
  " myte update-client --subject \"Weekly client update\" --body-markdown \"## Progress\\n- Login complete\" --target-contact-ids 507f1f77bcf86cd799439011,507f1f77bcf86cd799439012 --confirm-write --approval-artifact ./updates/week-12.md",
447
477
  " myte create-prd ./drafts/auth-prd.md --description \"Short card summary\" --confirm-write --approval-artifact ./drafts/auth-prd.md",
448
478
  " myte create-prd ./drafts/auth-prd.md ./drafts/billing-prd.md --confirm-write --approval-artifact ./drafts/prd-batch.md",
479
+ " myte create-prd ./drafts/overview.md ./drafts/api.md ./drafts/web.md --document-set --title \"Platform PRD\" --confirm-write --approval-artifact ./drafts/platform-prd-approval.md",
449
480
  " cat ./drafts/auth-prd.md | myte create-prd --stdin --confirm-write --approval-artifact ./drafts/auth-prd.md",
450
481
  " myte config",
451
482
  ].join("\n");
@@ -669,14 +700,280 @@ function parseFeedbackRequestIdsArg(args) {
669
700
  );
670
701
  }
671
702
 
672
- function sleep(ms) {
673
- return new Promise((resolve) => setTimeout(resolve, ms));
674
- }
675
-
676
- async function getFetch() {
677
- if (typeof fetch !== "undefined") return fetch;
678
- throw new Error("Global fetch is unavailable. myte requires Node 18+.");
679
- }
703
+ function sleep(ms) {
704
+ return new Promise((resolve) => setTimeout(resolve, ms));
705
+ }
706
+
707
+ function proxyEnvironmentValue(env, lowerName, upperName) {
708
+ const lowerValue = String(env?.[lowerName] || "").trim();
709
+ if (lowerValue) return lowerValue;
710
+ return String(env?.[upperName] || "").trim();
711
+ }
712
+
713
+ function proxyEnvironmentSummary(env = process.env) {
714
+ const httpProxy = proxyEnvironmentValue(env, "http_proxy", "HTTP_PROXY");
715
+ const httpsProxy = proxyEnvironmentValue(env, "https_proxy", "HTTPS_PROXY");
716
+ const noProxy = proxyEnvironmentValue(env, "no_proxy", "NO_PROXY");
717
+ return {
718
+ configured: Boolean(httpProxy || httpsProxy),
719
+ http_proxy_present: Boolean(httpProxy),
720
+ https_proxy_present: Boolean(httpsProxy),
721
+ no_proxy_present: Boolean(noProxy),
722
+ };
723
+ }
724
+
725
+ function buildFetchRuntime({
726
+ env = process.env,
727
+ nativeFetch = globalThis.fetch,
728
+ undiciModule,
729
+ } = {}) {
730
+ const proxySummary = proxyEnvironmentSummary(env);
731
+ if (!proxySummary.configured) {
732
+ if (typeof nativeFetch !== "function") {
733
+ const error = new Error("Global fetch is unavailable. myte requires Node 18.17+.");
734
+ error.code = "MYTE_FETCH_UNAVAILABLE";
735
+ throw error;
736
+ }
737
+ return {
738
+ fetch: nativeFetch,
739
+ transport: "node_fetch",
740
+ proxy: proxySummary,
741
+ };
742
+ }
743
+
744
+ let undici = undiciModule;
745
+ if (!undici) {
746
+ try {
747
+ undici = require("undici");
748
+ } catch (cause) {
749
+ const error = new Error(
750
+ "Proxy variables are configured, but the Myte HTTP transport is unavailable. Reinstall the myte package.",
751
+ );
752
+ error.code = "MYTE_PROXY_TRANSPORT_UNAVAILABLE";
753
+ error.cause = cause;
754
+ throw error;
755
+ }
756
+ }
757
+ if (typeof undici.fetch !== "function" || typeof undici.EnvHttpProxyAgent !== "function") {
758
+ const error = new Error(
759
+ "The installed Myte HTTP transport does not support HTTP_PROXY, HTTPS_PROXY, and NO_PROXY.",
760
+ );
761
+ error.code = "MYTE_PROXY_TRANSPORT_INCOMPATIBLE";
762
+ throw error;
763
+ }
764
+
765
+ const dispatcher = new undici.EnvHttpProxyAgent({
766
+ httpProxy: proxyEnvironmentValue(env, "http_proxy", "HTTP_PROXY") || undefined,
767
+ httpsProxy: proxyEnvironmentValue(env, "https_proxy", "HTTPS_PROXY") || undefined,
768
+ noProxy: proxyEnvironmentValue(env, "no_proxy", "NO_PROXY") || undefined,
769
+ });
770
+ return {
771
+ fetch: (url, options = {}) => undici.fetch(url, { ...options, dispatcher }),
772
+ transport: "undici_env_proxy",
773
+ proxy: proxySummary,
774
+ };
775
+ }
776
+
777
+ let fetchRuntimePromise = null;
778
+
779
+ async function getFetchRuntime() {
780
+ if (!fetchRuntimePromise) {
781
+ fetchRuntimePromise = Promise.resolve().then(() => buildFetchRuntime());
782
+ }
783
+ return fetchRuntimePromise;
784
+ }
785
+
786
+ async function getFetch() {
787
+ return (await getFetchRuntime()).fetch;
788
+ }
789
+
790
+ function requestErrorChain(error) {
791
+ const chain = [];
792
+ const seen = new Set();
793
+ let current = error;
794
+ while (current && typeof current === "object" && !seen.has(current) && chain.length < 8) {
795
+ seen.add(current);
796
+ chain.push(current);
797
+ current = current.cause;
798
+ }
799
+ return chain;
800
+ }
801
+
802
+ function requestErrorCode(error) {
803
+ for (const item of requestErrorChain(error)) {
804
+ const code = String(item?.code || item?.errno || "").trim();
805
+ if (code) return code;
806
+ }
807
+ return "";
808
+ }
809
+
810
+ function requestErrorMessage(error) {
811
+ return requestErrorChain(error)
812
+ .map((item) => String(item?.message || "").trim())
813
+ .filter(Boolean)
814
+ .join(" ");
815
+ }
816
+
817
+ function safeEndpointHost(url) {
818
+ try {
819
+ return new URL(String(url || "")).host || "unknown";
820
+ } catch {
821
+ return "unknown";
822
+ }
823
+ }
824
+
825
+ function classifyNetworkFailure(error, { timedOut = false } = {}) {
826
+ const code = timedOut ? "ETIMEDOUT" : requestErrorCode(error) || "NETWORK_ERROR";
827
+ const message = requestErrorMessage(error).toLowerCase();
828
+ const certificateFailure =
829
+ /^(CERT_|ERR_TLS_CERT_|DEPTH_ZERO_SELF_SIGNED_CERT|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE)/.test(code) ||
830
+ /certificate|self[- ]signed|unable to verify|hostname\/ip does not match/.test(message);
831
+
832
+ if (timedOut || code === "ABORT_ERR" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ETIMEDOUT") {
833
+ return {
834
+ kind: "timeout",
835
+ layer: "network",
836
+ code,
837
+ detail: "The request exceeded its configured timeout before a complete HTTP response was received.",
838
+ guidance: "Retry on a stable network or increase --timeout-ms when the API is known to be reachable.",
839
+ transient: true,
840
+ };
841
+ }
842
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
843
+ return {
844
+ kind: "dns",
845
+ layer: "dns",
846
+ code,
847
+ detail: "The API hostname could not be resolved.",
848
+ guidance: "Check DNS connectivity, captive-portal login, VPN, or local resolver settings.",
849
+ transient: true,
850
+ };
851
+ }
852
+ if (code === "ECONNREFUSED") {
853
+ return {
854
+ kind: "connection_refused",
855
+ layer: "tcp",
856
+ code,
857
+ detail: "The remote host refused the TCP connection.",
858
+ guidance: "Check the API base, proxy/VPN path, firewall policy, and service availability.",
859
+ transient: true,
860
+ };
861
+ }
862
+ if (certificateFailure) {
863
+ return {
864
+ kind: "tls_certificate",
865
+ layer: "tls",
866
+ code,
867
+ detail: "TLS certificate validation failed.",
868
+ guidance: "Check system time, trusted corporate certificates, proxy interception, and the requested hostname. Do not disable TLS verification.",
869
+ transient: false,
870
+ };
871
+ }
872
+ if (
873
+ code === "ECONNRESET" ||
874
+ code === "UND_ERR_SOCKET" ||
875
+ /secure tls connection|tls connection|socket disconnected|connection reset|other side closed/.test(message)
876
+ ) {
877
+ return {
878
+ kind: "connection_reset",
879
+ layer: "tls",
880
+ code,
881
+ detail: /before secure tls|secure tls connection/.test(message)
882
+ ? "The TLS connection was reset before the secure connection was established."
883
+ : "The network connection was reset before a complete HTTP response was received.",
884
+ guidance: "The API key was loaded, but the request did not reach the Myte API. Check captive-portal login, restricted Wi-Fi, proxy/VPN settings, or retry on another network.",
885
+ transient: true,
886
+ };
887
+ }
888
+ if (/ssl|tls|handshake/.test(message) || code.startsWith("ERR_SSL")) {
889
+ return {
890
+ kind: "tls",
891
+ layer: "tls",
892
+ code,
893
+ detail: "The TLS handshake failed before an HTTP response was received.",
894
+ guidance: "Check captive-portal login, proxy/VPN interception, system trust, or retry on another network.",
895
+ transient: true,
896
+ };
897
+ }
898
+ return {
899
+ kind: "network",
900
+ layer: "network",
901
+ code,
902
+ detail: "The transport failed before a complete HTTP response was received.",
903
+ guidance: "Run `myte doctor --json`, then check network, proxy/VPN, captive-portal, and API base settings.",
904
+ transient: true,
905
+ };
906
+ }
907
+
908
+ function createNetworkRequestError(error, url, { timedOut = false } = {}) {
909
+ if (error?.isMyteRequestError) return error;
910
+ const classified = classifyNetworkFailure(error, { timedOut });
911
+ const endpoint = safeEndpointHost(url);
912
+ const wrapped = new Error(
913
+ [
914
+ "Network request failed before receiving an HTTP response.",
915
+ `Endpoint: ${endpoint}`,
916
+ `Cause: ${classified.code}`,
917
+ `Detail: ${classified.detail}`,
918
+ classified.guidance,
919
+ ].join("\n"),
920
+ );
921
+ wrapped.name = "MyteNetworkError";
922
+ wrapped.code = classified.code;
923
+ wrapped.failureKind = classified.kind;
924
+ wrapped.failureLayer = classified.layer;
925
+ wrapped.endpoint = endpoint;
926
+ wrapped.detail = classified.detail;
927
+ wrapped.guidance = classified.guidance;
928
+ wrapped.transient = classified.transient;
929
+ wrapped.isMyteRequestError = true;
930
+ wrapped.cause = error;
931
+ return wrapped;
932
+ }
933
+
934
+ function createNonJsonResponseError(resp, url) {
935
+ const status = Number(resp?.status || 0);
936
+ const contentType = String(resp?.headers?.get?.("content-type") || "").toLowerCase();
937
+ const endpoint = safeEndpointHost(url);
938
+ const likelyPortal = status >= 200 && status < 400 && contentType.includes("text/html");
939
+ const detail = likelyPortal
940
+ ? "The endpoint returned HTML instead of JSON, which commonly indicates a captive portal or intercepting proxy."
941
+ : "The endpoint returned a non-JSON response, so the CLI could not validate the API contract.";
942
+ const error = new Error(
943
+ [
944
+ `Invalid HTTP response (${status || "unknown status"}).`,
945
+ `Endpoint: ${endpoint}`,
946
+ `Cause: ${likelyPortal ? "CAPTIVE_PORTAL_OR_PROXY" : "NON_JSON_RESPONSE"}`,
947
+ `Detail: ${detail}`,
948
+ ].join("\n"),
949
+ );
950
+ error.name = "MyteHttpError";
951
+ error.code = likelyPortal ? "CAPTIVE_PORTAL_OR_PROXY" : "NON_JSON_RESPONSE";
952
+ error.failureKind = likelyPortal ? "captive_portal" : "non_json_response";
953
+ error.failureLayer = "http";
954
+ error.endpoint = endpoint;
955
+ error.status = status;
956
+ error.transient = likelyPortal || [408, 429, 500, 502, 503, 504].includes(status);
957
+ error.isMyteRequestError = true;
958
+ const retryAfter = resp?.headers?.get?.("retry-after");
959
+ const requestId = resp?.headers?.get?.("x-request-id");
960
+ if (retryAfter) error.retryAfter = retryAfter;
961
+ if (requestId) error.requestId = String(requestId);
962
+ return error;
963
+ }
964
+
965
+ function publicRequestFailure(error) {
966
+ return {
967
+ layer: error?.failureLayer || (error?.status ? "http" : "unknown"),
968
+ kind: error?.failureKind || "unknown",
969
+ code: String(error?.code || error?.status || "UNKNOWN"),
970
+ status: Number(error?.status || 0) || null,
971
+ endpoint: error?.endpoint || null,
972
+ detail: error?.detail || String(error?.message || "Request failed.").split("\n")[0],
973
+ transient: Boolean(error?.transient),
974
+ request_id: error?.requestId || null,
975
+ };
976
+ }
680
977
 
681
978
  function normalizeApiBase(baseRaw) {
682
979
  const baseTrim = String(baseRaw || "").trim().replace(/\/+$/, "");
@@ -684,24 +981,31 @@ function normalizeApiBase(baseRaw) {
684
981
  return base.endsWith("/api") ? base : `${base}/api`;
685
982
  }
686
983
 
687
- async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
688
- const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
689
- const timeoutId =
690
- controller && timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
691
- try {
692
- const resp = await fetchFn(url, { ...options, signal: controller?.signal });
693
- const text = await resp.text();
694
- let body;
695
- try {
696
- body = JSON.parse(text);
697
- } catch {
698
- const err = new Error(`Non-JSON response (${resp.status}): ${text.slice(0, 500)}`);
699
- err.status = resp.status;
700
- const retryAfter = resp.headers?.get?.("retry-after");
701
- if (retryAfter) err.retryAfter = retryAfter;
702
- throw err;
703
- }
704
- return { resp, body };
984
+ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
985
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
986
+ let timedOut = false;
987
+ const timeoutId =
988
+ controller && timeoutMs > 0
989
+ ? setTimeout(() => {
990
+ timedOut = true;
991
+ controller.abort();
992
+ }, timeoutMs)
993
+ : undefined;
994
+ try {
995
+ let resp;
996
+ try {
997
+ resp = await fetchFn(url, { ...options, signal: controller?.signal });
998
+ } catch (error) {
999
+ throw createNetworkRequestError(error, url, { timedOut });
1000
+ }
1001
+ const text = await resp.text();
1002
+ let body;
1003
+ try {
1004
+ body = JSON.parse(text);
1005
+ } catch {
1006
+ throw createNonJsonResponseError(resp, url);
1007
+ }
1008
+ return { resp, body };
705
1009
  } finally {
706
1010
  if (timeoutId) clearTimeout(timeoutId);
707
1011
  }
@@ -1635,14 +1939,11 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
1635
1939
  headers: { Authorization: `Bearer ${key}` },
1636
1940
  },
1637
1941
  timeoutMs
1638
- );
1639
-
1640
- if (!resp.ok || body.status !== "success") {
1641
- const msg = body?.message || `Config request failed (${resp.status})`;
1642
- const err = new Error(msg);
1643
- err.status = resp.status;
1644
- throw err;
1645
- }
1942
+ );
1943
+
1944
+ if (!resp.ok || body.status !== "success") {
1945
+ throw createHttpResponseError(resp, body, url, "Project configuration could not be loaded.");
1946
+ }
1646
1947
  return body.data || {};
1647
1948
  }
1648
1949
 
@@ -1696,7 +1997,7 @@ async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
1696
1997
  return body.data || {};
1697
1998
  }
1698
1999
 
1699
- async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {} }) {
2000
+ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {} }) {
1700
2001
  const fetchFn = await getFetch();
1701
2002
  const url = new URL(`${apiBase}/project-assistant/feedback-sync`);
1702
2003
  if (filters.status) url.searchParams.set("status", String(filters.status));
@@ -1704,9 +2005,21 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
1704
2005
  if (filters.includePrdText !== undefined) {
1705
2006
  url.searchParams.set("include_prd_text", filters.includePrdText ? "true" : "false");
1706
2007
  }
1707
- if (filters.includeCommentTurns !== undefined) {
1708
- url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
1709
- }
2008
+ if (filters.includeCommentTurns !== undefined) {
2009
+ url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
2010
+ }
2011
+ if (filters.limit !== undefined && filters.limit !== null && filters.limit !== "") {
2012
+ url.searchParams.set("limit", String(filters.limit));
2013
+ }
2014
+ if (filters.offset !== undefined && filters.offset !== null && Number(filters.offset) > 0) {
2015
+ url.searchParams.set("offset", String(filters.offset));
2016
+ }
2017
+ if (filters.paginationMode) {
2018
+ url.searchParams.set("pagination_mode", String(filters.paginationMode));
2019
+ }
2020
+ if (filters.cursor) {
2021
+ url.searchParams.set("cursor", String(filters.cursor));
2022
+ }
1710
2023
  const { resp, body } = await fetchJsonWithTimeout(
1711
2024
  fetchFn,
1712
2025
  url.toString(),
@@ -1723,8 +2036,191 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
1723
2036
  err.status = resp.status;
1724
2037
  throw err;
1725
2038
  }
1726
- return body.data || {};
1727
- }
2039
+ return body.data || {};
2040
+ }
2041
+
2042
+ function resolveBoundedInteger(value, { fallback, min, max, label }) {
2043
+ if (value === undefined || value === null || value === "") return fallback;
2044
+ const parsed = Number(value);
2045
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
2046
+ throw new Error(`${label} must be an integer between ${min} and ${max}.`);
2047
+ }
2048
+ return parsed;
2049
+ }
2050
+
2051
+ function aggregateFeedbackSyncCounts(items, availableTotal) {
2052
+ const byStatus = {};
2053
+ const bySource = {};
2054
+ let withPrdText = 0;
2055
+ let withConversationTurns = 0;
2056
+ for (const item of items) {
2057
+ const status = String(item?.status || "Pending");
2058
+ const source = String(item?.source || "User");
2059
+ byStatus[status] = (byStatus[status] || 0) + 1;
2060
+ bySource[source] = (bySource[source] || 0) + 1;
2061
+ if (String(item?.prd_text || "").trim()) withPrdText += 1;
2062
+ if (Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0) {
2063
+ withConversationTurns += 1;
2064
+ }
2065
+ }
2066
+ return {
2067
+ total_feedback: items.length,
2068
+ available_feedback: Number.isFinite(Number(availableTotal)) ? Number(availableTotal) : items.length,
2069
+ with_prd_text: withPrdText,
2070
+ with_conversation_turns: withConversationTurns,
2071
+ by_status: byStatus,
2072
+ by_source: bySource,
2073
+ };
2074
+ }
2075
+
2076
+ async function fetchCompleteFeedbackSyncSnapshot({
2077
+ apiBase,
2078
+ key,
2079
+ timeoutMs,
2080
+ filters = {},
2081
+ pageSize = DEFAULT_FEEDBACK_SYNC_PAGE_SIZE,
2082
+ maxItems = 0,
2083
+ onPage,
2084
+ }) {
2085
+ const items = [];
2086
+ const pageHashes = [];
2087
+ const stageTotals = {};
2088
+ let firstSnapshot = null;
2089
+ let offset = 0;
2090
+ let availableTotal = null;
2091
+ let pageCount = 0;
2092
+ let sawPagination = false;
2093
+ let cursor = null;
2094
+ let useCursorPagination = true;
2095
+ let serverHasMore = false;
2096
+ const seenFeedbackIds = new Set();
2097
+
2098
+ while (true) {
2099
+ const remaining = maxItems > 0 ? maxItems - items.length : pageSize;
2100
+ if (maxItems > 0 && remaining <= 0) break;
2101
+ const requestedLimit = Math.min(pageSize, maxItems > 0 ? remaining : pageSize);
2102
+ const page = await fetchFeedbackSyncSnapshot({
2103
+ apiBase,
2104
+ key,
2105
+ timeoutMs,
2106
+ filters: {
2107
+ ...filters,
2108
+ limit: requestedLimit,
2109
+ ...(useCursorPagination
2110
+ ? {
2111
+ paginationMode: "cursor",
2112
+ ...(cursor ? { cursor } : {}),
2113
+ }
2114
+ : { offset }),
2115
+ },
2116
+ });
2117
+ pageCount += 1;
2118
+ if (!firstSnapshot) firstSnapshot = page;
2119
+
2120
+ const pageItems = Array.isArray(page.items) ? page.items : [];
2121
+ const pagination = page.pagination && typeof page.pagination === "object" ? page.pagination : null;
2122
+ sawPagination = sawPagination || Boolean(pagination);
2123
+ if (
2124
+ availableTotal === null &&
2125
+ pagination &&
2126
+ Number.isFinite(Number(pagination.total))
2127
+ ) {
2128
+ availableTotal = Number(pagination.total);
2129
+ }
2130
+ if (page.snapshot_hash) pageHashes.push(String(page.snapshot_hash));
2131
+ const stages = page?.diagnostics?.stages_ms;
2132
+ if (stages && typeof stages === "object") {
2133
+ for (const [name, value] of Object.entries(stages)) {
2134
+ const duration = Number(value);
2135
+ if (Number.isFinite(duration)) stageTotals[name] = (stageTotals[name] || 0) + duration;
2136
+ }
2137
+ }
2138
+
2139
+ for (const item of pageItems) {
2140
+ const feedbackId = firstNonEmptyString(item?.feedback_id, item?._id);
2141
+ if (feedbackId && seenFeedbackIds.has(feedbackId)) {
2142
+ throw new Error(
2143
+ `Feedback sync snapshot changed while paging; duplicate feedback ${feedbackId} was returned. Retry the sync.`,
2144
+ );
2145
+ }
2146
+ if (feedbackId) seenFeedbackIds.add(feedbackId);
2147
+ items.push(item);
2148
+ }
2149
+ if (typeof onPage === "function") {
2150
+ onPage({
2151
+ page: pageCount,
2152
+ received: pageItems.length,
2153
+ synced: items.length,
2154
+ total: availableTotal,
2155
+ });
2156
+ }
2157
+
2158
+ if (!pagination) break;
2159
+ const responseUsesCursor = String(pagination.mode || "").toLowerCase() === "cursor";
2160
+ if (!responseUsesCursor) useCursorPagination = false;
2161
+ const hasMore = Boolean(pagination.has_more);
2162
+ serverHasMore = hasMore;
2163
+ if (!hasMore) break;
2164
+ if (pageItems.length === 0) {
2165
+ throw new Error(`Feedback sync page ${pageCount} reported more results but returned no items.`);
2166
+ }
2167
+ if (responseUsesCursor) {
2168
+ const nextCursor = firstNonEmptyString(pagination.next_cursor);
2169
+ if (!nextCursor || nextCursor === cursor) {
2170
+ throw new Error(
2171
+ `Feedback sync page ${pageCount} did not return a new cursor while more results remain.`,
2172
+ );
2173
+ }
2174
+ cursor = nextCursor;
2175
+ useCursorPagination = true;
2176
+ } else {
2177
+ useCursorPagination = false;
2178
+ offset += pageItems.length;
2179
+ }
2180
+ }
2181
+
2182
+ const base = firstSnapshot || {};
2183
+ if (!sawPagination && pageCount === 1) {
2184
+ return {
2185
+ ...base,
2186
+ diagnostics: {
2187
+ ...(base.diagnostics && typeof base.diagnostics === "object" ? base.diagnostics : {}),
2188
+ page_count: 1,
2189
+ },
2190
+ };
2191
+ }
2192
+ const aggregateHash = createHash("sha256")
2193
+ .update(JSON.stringify({
2194
+ project_id: base?.project?.id || null,
2195
+ item_hashes: items.map((item) => item?.snapshot_hash || item?.feedback_id || null),
2196
+ page_hashes: pageHashes,
2197
+ }))
2198
+ .digest("hex");
2199
+
2200
+ return {
2201
+ ...base,
2202
+ items,
2203
+ counts: aggregateFeedbackSyncCounts(items, availableTotal),
2204
+ pagination: {
2205
+ mode: useCursorPagination && sawPagination ? "cursor" : "offset",
2206
+ total: availableTotal ?? items.length,
2207
+ limit: pageSize,
2208
+ offset: 0,
2209
+ has_more:
2210
+ maxItems > 0
2211
+ ? serverHasMore || (availableTotal !== null && items.length < availableTotal)
2212
+ : false,
2213
+ page_count: pageCount,
2214
+ synced_count: items.length,
2215
+ next_cursor: maxItems > 0 && serverHasMore ? cursor : null,
2216
+ },
2217
+ snapshot_hash: aggregateHash,
2218
+ diagnostics: {
2219
+ page_count: pageCount,
2220
+ stages_ms: stageTotals,
2221
+ },
2222
+ };
2223
+ }
1728
2224
 
1729
2225
  async function fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId }) {
1730
2226
  const fetchFn = await getFetch();
@@ -2069,10 +2565,19 @@ async function fetchFeedbackPrdVersions({ apiBase, key, timeoutMs, feedbackId })
2069
2565
  return body.data || {};
2070
2566
  }
2071
2567
 
2072
- async function fetchFeedbackPrdVersionDiff({ apiBase, key, timeoutMs, feedbackId, versionId, compareTo }) {
2568
+ async function fetchFeedbackPrdVersionDiff({
2569
+ apiBase,
2570
+ key,
2571
+ timeoutMs,
2572
+ feedbackId,
2573
+ versionId,
2574
+ compareTo,
2575
+ documentId,
2576
+ }) {
2073
2577
  const fetchFn = await getFetch();
2074
- const url = new URL(`${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/prd/versions/${encodeURIComponent(String(versionId || ""))}/diff`);
2075
- if (compareTo) url.searchParams.set("compare_to", String(compareTo));
2578
+ const url = new URL(`${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/prd/versions/${encodeURIComponent(String(versionId || ""))}/diff`);
2579
+ if (compareTo) url.searchParams.set("compare_to", String(compareTo));
2580
+ if (documentId) url.searchParams.set("document_id", String(documentId));
2076
2581
  const { resp, body } = await fetchJsonWithTimeout(
2077
2582
  fetchFn,
2078
2583
  url.toString(),
@@ -2284,48 +2789,162 @@ async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
2284
2789
  return body.data || {};
2285
2790
  }
2286
2791
 
2287
- function resolveRetryAfterMs(err, fallbackMs = 5_000) {
2288
- const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
2289
- const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
2290
- if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
2291
- return Math.min(retryAfterSeconds * 1_000, 60_000);
2292
- }
2293
- return fallbackMs;
2294
- }
2295
-
2296
- function isTransientQueryStatusError(err) {
2297
- return [408, 429, 500, 502, 503, 504].includes(Number(err?.status));
2298
- }
2299
-
2300
- async function createAssistantQueryJob({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
2301
- const fetchFn = await getFetch();
2302
- const url = `${apiBase}${endpoint}`;
2303
- const { resp, body } = await fetchJsonWithTimeout(
2792
+ function resolveRetryAfterMs(err, fallbackMs = 5_000) {
2793
+ const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
2794
+ const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
2795
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
2796
+ return Math.min(retryAfterSeconds * 1_000, 60_000);
2797
+ }
2798
+ const retryAfterDate = Date.parse(String(retryAfterRaw || ""));
2799
+ if (Number.isFinite(retryAfterDate)) {
2800
+ return Math.min(Math.max(0, retryAfterDate - Date.now()), 60_000);
2801
+ }
2802
+ return fallbackMs;
2803
+ }
2804
+
2805
+ function isTransientQueryStatusError(err) {
2806
+ return Boolean(
2807
+ err?.transient ||
2808
+ ["dns", "tcp", "tls", "network"].includes(String(err?.failureLayer || "")) ||
2809
+ [408, 429, 500, 502, 503, 504].includes(Number(err?.status)),
2810
+ );
2811
+ }
2812
+
2813
+ function createHttpResponseError(resp, body, url, fallbackMessage) {
2814
+ const status = Number(resp?.status || 0);
2815
+ const endpoint = safeEndpointHost(url);
2816
+ let kind = "http";
2817
+ let detail = fallbackMessage || `HTTP request failed (${status || "unknown status"}).`;
2818
+ let transient = false;
2819
+ if (status === 401) {
2820
+ kind = "authentication";
2821
+ detail = "The API rejected the project key.";
2822
+ } else if (status === 403) {
2823
+ kind = "authorization";
2824
+ detail = "The authenticated project identity is not allowed to perform this action.";
2825
+ } else if (status === 429) {
2826
+ kind = "rate_limit";
2827
+ detail = "The API rate limit is temporarily exhausted.";
2828
+ transient = true;
2829
+ } else if ([408, 500, 502, 503, 504].includes(status)) {
2830
+ kind = "server";
2831
+ detail = "The API or an upstream gateway returned a transient error.";
2832
+ transient = true;
2833
+ }
2834
+ const requestId =
2835
+ resp?.headers?.get?.("x-request-id") ||
2836
+ (body && typeof body === "object" ? body.request_id || body?.data?.request_id : null);
2837
+ const error = new Error(
2838
+ [
2839
+ `HTTP request failed (${status || "unknown status"}).`,
2840
+ `Endpoint: ${endpoint}`,
2841
+ `Cause: ${kind.toUpperCase()}`,
2842
+ `Detail: ${detail}`,
2843
+ requestId ? `Request ID: ${requestId}` : "",
2844
+ ]
2845
+ .filter(Boolean)
2846
+ .join("\n"),
2847
+ );
2848
+ error.name = "MyteHttpError";
2849
+ error.code = `HTTP_${status || "ERROR"}`;
2850
+ error.status = status;
2851
+ error.failureKind = kind;
2852
+ error.failureLayer = "http";
2853
+ error.endpoint = endpoint;
2854
+ error.detail = detail;
2855
+ error.transient = transient;
2856
+ error.isMyteRequestError = true;
2857
+ if (requestId) error.requestId = String(requestId);
2858
+ const retryAfter = resp?.headers?.get?.("retry-after");
2859
+ if (retryAfter) error.retryAfter = retryAfter;
2860
+ return error;
2861
+ }
2862
+
2863
+ function queryRequestId(args) {
2864
+ const explicit = firstNonEmptyString(
2865
+ args["request-id"],
2866
+ args.requestId,
2867
+ args.request_id,
2868
+ args["idempotency-key"],
2869
+ args.idempotencyKey,
2870
+ args.idempotency_key,
2871
+ );
2872
+ const value = explicit || `myte-query-${randomUUID()}`;
2873
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
2874
+ throw new Error("request-id must contain 1-128 safe letters, numbers, dots, underscores, colons, or hyphens.");
2875
+ }
2876
+ return value;
2877
+ }
2878
+
2879
+ function transientRetryDelayMs(error, attempt, initialDelayMs = 750) {
2880
+ const exponential = Math.min(15_000, initialDelayMs * 2 ** Math.max(0, attempt - 1));
2881
+ const fallback = resolveRetryAfterMs(error, exponential);
2882
+ const jitterLimit = Math.max(0, Number(process.env.MYTE_RETRY_JITTER_MS ?? 250) || 0);
2883
+ const jitter = jitterLimit ? Math.floor(Math.random() * (jitterLimit + 1)) : 0;
2884
+ return Math.min(60_000, fallback + jitter);
2885
+ }
2886
+
2887
+ async function withTransientQueryRetries(
2888
+ operation,
2889
+ {
2890
+ maxAttempts = 3,
2891
+ verbose = false,
2892
+ label = "request",
2893
+ initialDelayMs = 750,
2894
+ } = {},
2895
+ ) {
2896
+ let lastError;
2897
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2898
+ try {
2899
+ return await operation(attempt);
2900
+ } catch (error) {
2901
+ lastError = error;
2902
+ if (!isTransientQueryStatusError(error) || attempt >= maxAttempts) throw error;
2903
+ const waitMs = transientRetryDelayMs(error, attempt, initialDelayMs);
2904
+ if (verbose) {
2905
+ console.error(
2906
+ `[myte] ${label} transient failure (${error?.code || error?.status || "unknown"}); retry ${attempt + 1}/${maxAttempts} in ${waitMs}ms.`,
2907
+ );
2908
+ }
2909
+ await sleep(waitMs);
2910
+ }
2911
+ }
2912
+ throw lastError;
2913
+ }
2914
+
2915
+ async function createAssistantQueryJob({
2916
+ apiBase,
2917
+ key,
2918
+ payload,
2919
+ timeoutMs,
2920
+ requestId,
2921
+ endpoint = "/project-assistant/query",
2922
+ }) {
2923
+ const fetchFn = await getFetch();
2924
+ const url = `${apiBase}${endpoint}`;
2925
+ const { resp, body } = await fetchJsonWithTimeout(
2304
2926
  fetchFn,
2305
2927
  url,
2306
2928
  {
2307
2929
  method: "POST",
2308
2930
  headers: {
2309
- "Content-Type": "application/json",
2310
- Authorization: `Bearer ${key}`,
2311
- },
2312
- body: JSON.stringify(payload),
2931
+ "Content-Type": "application/json",
2932
+ Authorization: `Bearer ${key}`,
2933
+ "X-Idempotency-Key": requestId,
2934
+ "X-Request-ID": requestId,
2935
+ },
2936
+ body: JSON.stringify({ ...payload, client_request_id: requestId }),
2313
2937
  },
2314
2938
  timeoutMs
2315
2939
  );
2316
2940
 
2317
- if (!resp.ok || body.status !== "success") {
2318
- const retryAfter = resp.headers?.get?.("retry-after");
2319
- const msg = body?.message || `Query failed (${resp.status})`;
2320
- const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
2321
- err.status = resp.status;
2322
- if (retryAfter) err.retryAfter = retryAfter;
2323
- throw err;
2324
- }
2325
- return body.data || {};
2326
- }
2327
-
2328
- async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId }) {
2941
+ if (!resp.ok || body.status !== "success") {
2942
+ throw createHttpResponseError(resp, body, url, "The query job could not be created.");
2943
+ }
2944
+ return body.data || {};
2945
+ }
2946
+
2947
+ async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId, requestId }) {
2329
2948
  const fetchFn = await getFetch();
2330
2949
  const url = `${apiBase}/project-assistant/query/${encodeURIComponent(String(jobId || ""))}`;
2331
2950
  const { resp, body } = await fetchJsonWithTimeout(
@@ -2333,19 +2952,17 @@ async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId })
2333
2952
  url,
2334
2953
  {
2335
2954
  method: "GET",
2336
- headers: { Authorization: `Bearer ${key}` },
2955
+ headers: {
2956
+ Authorization: `Bearer ${key}`,
2957
+ ...(requestId ? { "X-Request-ID": requestId } : {}),
2958
+ },
2337
2959
  },
2338
2960
  timeoutMs
2339
2961
  );
2340
2962
 
2341
- if (!resp.ok || body.status !== "success") {
2342
- const retryAfter = resp.headers?.get?.("retry-after");
2343
- const msg = body?.message || `Query status request failed (${resp.status})`;
2344
- const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
2345
- err.status = resp.status;
2346
- if (retryAfter) err.retryAfter = retryAfter;
2347
- throw err;
2348
- }
2963
+ if (!resp.ok || body.status !== "success") {
2964
+ throw createHttpResponseError(resp, body, url, "The query status could not be loaded.");
2965
+ }
2349
2966
  return body.data || {};
2350
2967
  }
2351
2968
 
@@ -3278,12 +3895,50 @@ function writeJsonFile(filePath, value) {
3278
3895
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
3279
3896
  }
3280
3897
 
3281
- function writeTextFile(filePath, value) {
3282
- ensureDir(path.dirname(filePath));
3283
- fs.writeFileSync(filePath, String(value || ""), "utf8");
3284
- }
3285
-
3286
- function sanitizeFileSegment(value, fallback = "item") {
3898
+ function writeTextFile(filePath, value) {
3899
+ ensureDir(path.dirname(filePath));
3900
+ fs.writeFileSync(filePath, String(value || ""), "utf8");
3901
+ }
3902
+
3903
+ function replacePathsTransaction(entries, token) {
3904
+ const prepared = [];
3905
+ try {
3906
+ for (const entry of entries) {
3907
+ if (!entry?.staged || !entry?.target || !fs.existsSync(entry.staged)) {
3908
+ throw new Error(`Missing staged sync artifact for ${entry?.target || "unknown target"}.`);
3909
+ }
3910
+ ensureDir(path.dirname(entry.target));
3911
+ const backup = `${entry.target}.myte-backup-${token}`;
3912
+ if (fs.existsSync(backup)) removePathIfExists(backup);
3913
+ if (fs.existsSync(entry.target)) fs.renameSync(entry.target, backup);
3914
+ prepared.push({ ...entry, backup, installed: false });
3915
+ }
3916
+
3917
+ for (const entry of prepared) {
3918
+ fs.renameSync(entry.staged, entry.target);
3919
+ entry.installed = true;
3920
+ }
3921
+ } catch (error) {
3922
+ for (const entry of [...prepared].reverse()) {
3923
+ if (entry.installed && fs.existsSync(entry.target)) removePathIfExists(entry.target);
3924
+ if (fs.existsSync(entry.backup)) fs.renameSync(entry.backup, entry.target);
3925
+ }
3926
+ throw error;
3927
+ }
3928
+
3929
+ for (const entry of prepared) {
3930
+ if (!fs.existsSync(entry.backup)) continue;
3931
+ try {
3932
+ removePathIfExists(entry.backup);
3933
+ } catch (error) {
3934
+ console.warn(
3935
+ `[myte] Sync committed, but an old backup could not be removed: ${path.basename(entry.backup)}`,
3936
+ );
3937
+ }
3938
+ }
3939
+ }
3940
+
3941
+ function sanitizeFileSegment(value, fallback = "item") {
3287
3942
  const cleaned = String(value || "")
3288
3943
  .trim()
3289
3944
  .replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
@@ -3678,37 +4333,87 @@ function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
3678
4333
  };
3679
4334
  }
3680
4335
 
3681
- function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
3682
- const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
3683
- const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
3684
-
3685
- ensureDir(dataRoot);
3686
- ensureDir(prdSyncDir);
3687
- clearFileDirectory(prdSyncDir, [".md", ".markdown", ".txt"]);
3688
- pruneLegacyCommandCenterArtifacts(dataRoot, { bootstrap: true, feedback: true });
3689
-
3690
- if (snapshot.project && typeof snapshot.project === "object") {
3691
- writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
3692
- }
3693
-
3694
- const items = Array.isArray(snapshot.items) ? snapshot.items : [];
3695
- let prdFileCount = 0;
3696
- const materializedItems = items.map((rawItem, index) => {
3697
- const item = isPlainObject(rawItem) ? { ...rawItem } : {};
3698
- const feedbackId = stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`);
3699
- const conversationTurns = normalizeFeedbackConversationTurns(item.conversation_turns);
3700
- const prdText = String(item.prd_text || "").trim();
3701
- const attachmentDocuments = Array.isArray(item.attachment_documents) ? item.attachment_documents : [];
3702
-
3703
- let contextSource = "description_only";
3704
- let contextNote = "No separate PRD. Use feedback_text as the context for this feedback item.";
3705
- let prdFile = null;
3706
-
3707
- if (prdText) {
3708
- const prdFilename = `${sanitizeFileSegment(feedbackId, `feedback-${index + 1}`)}.md`;
3709
- const prdPath = path.join(prdSyncDir, prdFilename);
3710
- writeTextFile(prdPath, ensureTrailingNewline(prdText));
3711
- prdFile = toPosixRelativePath(targetRoot, prdPath);
4336
+ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
4337
+ const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
4338
+ const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
4339
+ const transactionToken = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
4340
+ const stagingRoot = path.join(targetRoot, `.myte-feedback-sync-${transactionToken}`);
4341
+ const stagedDataRoot = path.join(stagingRoot, "data");
4342
+ const stagedPrdRoot = path.join(stagingRoot, "PRD", "feedback-sync");
4343
+ ensureDir(stagedDataRoot);
4344
+ ensureDir(stagedPrdRoot);
4345
+
4346
+ const items = Array.isArray(snapshot.items) ? snapshot.items : [];
4347
+ let prdFileCount = 0;
4348
+ let prdDocumentSetCount = 0;
4349
+ const materializedItems = items.map((rawItem, index) => {
4350
+ const item = isPlainObject(rawItem) ? { ...rawItem } : {};
4351
+ const feedbackId = stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`);
4352
+ const conversationTurns = normalizeFeedbackConversationTurns(item.conversation_turns);
4353
+ const prdText = String(item.prd_text || "").trim();
4354
+ const rawPrdDocuments = Array.isArray(item.prd_documents) ? item.prd_documents : [];
4355
+ const attachmentDocuments = Array.isArray(item.attachment_documents) ? item.attachment_documents : [];
4356
+
4357
+ let contextSource = "description_only";
4358
+ let contextNote = "No separate PRD. Use feedback_text as the context for this feedback item.";
4359
+ let prdFile = null;
4360
+ let materializedPrdDocuments = rawPrdDocuments.map((document) =>
4361
+ isPlainObject(document) ? { ...document } : {}
4362
+ );
4363
+
4364
+ if (rawPrdDocuments.length > 0) {
4365
+ prdDocumentSetCount += 1;
4366
+ const feedbackFolderName = sanitizeFileSegment(feedbackId, `feedback-${index + 1}`);
4367
+ materializedPrdDocuments = rawPrdDocuments.map((rawDocument, documentIndex) => {
4368
+ const document = isPlainObject(rawDocument) ? { ...rawDocument } : {};
4369
+ const markdown = String(
4370
+ document.canonical_markdown || document.source_markdown || ""
4371
+ );
4372
+ let localFile = null;
4373
+ if (markdown.trim()) {
4374
+ const rawFilename = sanitizeFileSegment(
4375
+ document.filename || document.title,
4376
+ `document-${documentIndex + 1}.md`
4377
+ );
4378
+ const filename = /\.(md|markdown)$/i.test(rawFilename)
4379
+ ? rawFilename
4380
+ : `${rawFilename}.md`;
4381
+ const position = Number.isInteger(Number(document.position))
4382
+ ? Number(document.position) + 1
4383
+ : documentIndex + 1;
4384
+ const orderedFilename = `${String(position).padStart(2, "0")}-${filename}`;
4385
+ const stagedPath = path.join(stagedPrdRoot, feedbackFolderName, orderedFilename);
4386
+ ensureDir(path.dirname(stagedPath));
4387
+ writeTextFile(stagedPath, ensureTrailingNewline(markdown));
4388
+ localFile = toPosixRelativePath(
4389
+ targetRoot,
4390
+ path.join(prdSyncDir, feedbackFolderName, orderedFilename)
4391
+ );
4392
+ prdFileCount += 1;
4393
+ }
4394
+ return {
4395
+ ...document,
4396
+ local_file: localFile,
4397
+ };
4398
+ });
4399
+ const primaryDocumentId = String(item.primary_prd_document_id || "");
4400
+ const primaryDocument =
4401
+ materializedPrdDocuments.find(
4402
+ (document) => String(document.document_id || "") === primaryDocumentId
4403
+ ) || materializedPrdDocuments[0];
4404
+ prdFile = primaryDocument?.local_file || null;
4405
+ if (prdFile) {
4406
+ contextSource = "prd_document_set";
4407
+ contextNote = "Readable PRD documents are stored in the linked document files.";
4408
+ } else {
4409
+ contextSource = "prd_declared_but_unavailable";
4410
+ contextNote = "A PRD document set exists, but readable document text was not included in this sync snapshot.";
4411
+ }
4412
+ } else if (prdText) {
4413
+ const prdFilename = `${sanitizeFileSegment(feedbackId, `feedback-${index + 1}`)}.md`;
4414
+ const prdPath = path.join(stagedPrdRoot, prdFilename);
4415
+ writeTextFile(prdPath, ensureTrailingNewline(prdText));
4416
+ prdFile = toPosixRelativePath(targetRoot, path.join(prdSyncDir, prdFilename));
3712
4417
  contextSource = attachmentDocuments.length > 0 ? "attachment_file" : "prd_file";
3713
4418
  contextNote = attachmentDocuments.length > 0
3714
4419
  ? "Readable feedback attachment documents are stored in the linked file."
@@ -3720,31 +4425,34 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
3720
4425
  }
3721
4426
 
3722
4427
  return {
3723
- ...item,
3724
- feedback_id: feedbackId,
3725
- conversation_turns: conversationTurns,
3726
- context_source: contextSource,
3727
- context_note: contextNote,
3728
- prd_file: prdFile,
4428
+ ...item,
4429
+ feedback_id: feedbackId,
4430
+ conversation_turns: conversationTurns,
4431
+ prd_documents: materializedPrdDocuments,
4432
+ context_source: contextSource,
4433
+ context_note: contextNote,
4434
+ prd_file: prdFile,
3729
4435
  };
3730
4436
  });
3731
4437
  const snapshotCounts = snapshot.counts && typeof snapshot.counts === "object"
3732
4438
  ? { ...snapshot.counts }
3733
4439
  : { total_feedback: materializedItems.length };
3734
- snapshotCounts.with_prd_files = prdFileCount;
4440
+ snapshotCounts.with_prd_files = prdFileCount;
4441
+ snapshotCounts.with_prd_document_sets = prdDocumentSetCount;
3735
4442
  if (snapshotCounts.with_conversation_turns === undefined) {
3736
4443
  snapshotCounts.with_conversation_turns = materializedItems.filter((item) => Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0).length;
3737
4444
  }
3738
4445
 
3739
- const payload = scrubBootstrapValue({
4446
+ const payload = scrubBootstrapValue({
3740
4447
  schema_version: snapshot.schema_version || 2,
3741
4448
  project: snapshot.project || null,
3742
4449
  repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
3743
4450
  filters: snapshot.filters && typeof snapshot.filters === "object" ? snapshot.filters : {},
3744
4451
  counts: snapshotCounts,
3745
- artifacts: {
3746
- feedback_prd_root: "PRD/feedback-sync",
3747
- with_prd_files: prdFileCount,
4452
+ artifacts: {
4453
+ feedback_prd_root: "PRD/feedback-sync",
4454
+ with_prd_files: prdFileCount,
4455
+ with_prd_document_sets: prdDocumentSetCount,
3748
4456
  },
3749
4457
  queue: materializedItems
3750
4458
  .filter((item) => String(item?.status || "").trim().toLowerCase() !== "resolved")
@@ -3754,24 +4462,55 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
3754
4462
  priority: item?.priority || null,
3755
4463
  title: item?.title || null,
3756
4464
  })),
3757
- items: materializedItems.map((item) => {
3758
- const nextItem = { ...item };
3759
- delete nextItem.prd_text;
3760
- return nextItem;
3761
- }),
4465
+ items: materializedItems.map((item) => {
4466
+ const nextItem = { ...item };
4467
+ delete nextItem.prd_text;
4468
+ nextItem.prd_documents = (nextItem.prd_documents || []).map((rawDocument) => {
4469
+ const document = isPlainObject(rawDocument) ? { ...rawDocument } : {};
4470
+ delete document.source_markdown;
4471
+ delete document.canonical_markdown;
4472
+ return document;
4473
+ });
4474
+ return nextItem;
4475
+ }),
3762
4476
  pagination: snapshot.pagination && typeof snapshot.pagination === "object" ? snapshot.pagination : undefined,
3763
4477
  generated_at: snapshot.generated_at || null,
3764
- snapshot_hash: snapshot.snapshot_hash || null,
3765
- });
3766
- writeYamlFile(path.join(dataRoot, "feedback.yml"), payload);
3767
-
3768
- return {
3769
- targetRoot,
3770
- dataRoot,
3771
- prdSyncDir,
3772
- manifest: payload,
3773
- };
3774
- }
4478
+ snapshot_hash: snapshot.snapshot_hash || null,
4479
+ });
4480
+ try {
4481
+ const replacements = [];
4482
+ if (snapshot.project && typeof snapshot.project === "object") {
4483
+ const stagedProjectPath = path.join(stagedDataRoot, "project.yml");
4484
+ writeYamlFile(stagedProjectPath, scrubBootstrapValue(snapshot.project));
4485
+ replacements.push({
4486
+ staged: stagedProjectPath,
4487
+ target: path.join(dataRoot, "project.yml"),
4488
+ });
4489
+ }
4490
+ const stagedFeedbackPath = path.join(stagedDataRoot, "feedback.yml");
4491
+ writeYamlFile(stagedFeedbackPath, payload);
4492
+ replacements.push(
4493
+ {
4494
+ staged: stagedPrdRoot,
4495
+ target: prdSyncDir,
4496
+ },
4497
+ {
4498
+ staged: stagedFeedbackPath,
4499
+ target: path.join(dataRoot, "feedback.yml"),
4500
+ }
4501
+ );
4502
+ replacePathsTransaction(replacements, transactionToken);
4503
+ pruneLegacyCommandCenterArtifacts(dataRoot, { bootstrap: true, feedback: true });
4504
+ return {
4505
+ targetRoot,
4506
+ dataRoot,
4507
+ prdSyncDir,
4508
+ manifest: payload,
4509
+ };
4510
+ } finally {
4511
+ if (fs.existsSync(stagingRoot)) removePathIfExists(stagingRoot);
4512
+ }
4513
+ }
3775
4514
 
3776
4515
  function resolveFeedbackReviewPaths(args, { requireFeedback = false } = {}) {
3777
4516
  const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
@@ -3847,9 +4586,14 @@ function feedbackChange(from, to) {
3847
4586
  return { from: from === undefined ? null : from, to: to === undefined ? null : to };
3848
4587
  }
3849
4588
 
3850
- function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
3851
- const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
3852
- const artifact = {
4589
+ function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
4590
+ const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
4591
+ const documentId = firstNonEmptyString(
4592
+ args["document-id"],
4593
+ args.documentId,
4594
+ args.document_id
4595
+ );
4596
+ const artifact = {
3853
4597
  schema_version: 1,
3854
4598
  kind: "feedback_refinement",
3855
4599
  project_id: firstNonEmptyString(manifest?.project?.id, item?.project_id),
@@ -3857,9 +4601,10 @@ function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, c
3857
4601
  title_snapshot: firstNonEmptyString(item?.title),
3858
4602
  base_snapshot_hash: firstNonEmptyString(item?.snapshot_hash),
3859
4603
  base_updated_at: firstNonEmptyString(item?.updated_at),
3860
- review_action: action,
3861
- reason: reason || null,
3862
- changes,
4604
+ review_action: action,
4605
+ reason: reason || null,
4606
+ document_id: documentId || undefined,
4607
+ changes,
3863
4608
  force: Boolean(args.force) || undefined,
3864
4609
  client_session_id: clientSessionId || undefined,
3865
4610
  created_at: new Date().toISOString(),
@@ -4563,7 +5308,7 @@ async function reviewProjectSuggestions({ apiBase, key, timeoutMs, payload, idem
4563
5308
  });
4564
5309
  }
4565
5310
 
4566
- async function runCreatePrd(args) {
5311
+ async function runCreatePrd(args) {
4567
5312
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
4568
5313
  if (!key) {
4569
5314
  console.error("Missing MYTE_API_KEY (project key) in environment/.env");
@@ -4585,13 +5330,24 @@ async function runCreatePrd(args) {
4585
5330
  .flatMap((value) => toStringArray(value))
4586
5331
  .map((item) => item.trim())
4587
5332
  .filter(Boolean);
4588
- const providedPaths = explicitPaths.length ? explicitPaths.concat(positionalPaths) : positionalPaths;
4589
- const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && providedPaths.length === 0));
4590
-
4591
- if (useStdin && providedPaths.length) {
4592
- console.error("Use either file paths or --stdin for create-prd, not both.");
4593
- process.exit(1);
4594
- }
5333
+ const providedPaths = explicitPaths.length ? explicitPaths.concat(positionalPaths) : positionalPaths;
5334
+ const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && providedPaths.length === 0));
5335
+ const documentSetMode = Boolean(
5336
+ args["document-set"] || args.documentSet || args.document_set
5337
+ );
5338
+
5339
+ if (useStdin && providedPaths.length) {
5340
+ console.error("Use either file paths or --stdin for create-prd, not both.");
5341
+ process.exit(1);
5342
+ }
5343
+ if (documentSetMode && useStdin) {
5344
+ console.error("--document-set requires one to three markdown file paths; --stdin is not supported.");
5345
+ process.exit(1);
5346
+ }
5347
+ if (documentSetMode && (providedPaths.length < 1 || providedPaths.length > 3)) {
5348
+ console.error("--document-set requires between one and three markdown file paths.");
5349
+ process.exit(1);
5350
+ }
4595
5351
 
4596
5352
  const buildCreatePrdPayload = ({ sourceText, filePath, includeClientRef = false }) => {
4597
5353
  const trimmedSource = String(sourceText || "").trim();
@@ -4623,8 +5379,8 @@ async function runCreatePrd(args) {
4623
5379
  return payload;
4624
5380
  };
4625
5381
 
4626
- let payloads = [];
4627
- if (useStdin) {
5382
+ let payloads = [];
5383
+ if (useStdin) {
4628
5384
  try {
4629
5385
  payloads = [
4630
5386
  buildCreatePrdPayload({
@@ -4643,21 +5399,72 @@ async function runCreatePrd(args) {
4643
5399
  printHelp();
4644
5400
  process.exit(1);
4645
5401
  }
4646
- try {
4647
- payloads = providedPaths.map((providedPath) => {
4648
- const absPath = path.resolve(providedPath);
4649
- if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
4650
- throw new Error(`PRD file not found: ${absPath}`);
4651
- }
4652
- return buildCreatePrdPayload({
4653
- sourceText: fs.readFileSync(absPath, "utf8"),
4654
- filePath: absPath,
4655
- includeClientRef: providedPaths.length > 1,
4656
- });
4657
- });
4658
- } catch (err) {
4659
- console.error(err?.message || err);
4660
- process.exit(1);
5402
+ try {
5403
+ const resolvedFiles = providedPaths.map((providedPath) => {
5404
+ const absPath = path.resolve(providedPath);
5405
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
5406
+ throw new Error(`PRD file not found: ${absPath}`);
5407
+ }
5408
+ return {
5409
+ absPath,
5410
+ sourceText: fs.readFileSync(absPath, "utf8"),
5411
+ };
5412
+ });
5413
+ if (documentSetMode) {
5414
+ const documents = resolvedFiles.map(({ absPath, sourceText }, index) => {
5415
+ const extension = path.extname(absPath).toLowerCase();
5416
+ if (![".md", ".markdown"].includes(extension)) {
5417
+ throw new Error(`PRD document-set files must use .md or .markdown: ${absPath}`);
5418
+ }
5419
+ if (!String(sourceText || "").trim()) {
5420
+ throw new Error(`PRD content is empty: ${absPath}`);
5421
+ }
5422
+ if (Array.from(sourceText).length > MAX_PRD_DOCUMENT_MARKDOWN_CHARS) {
5423
+ throw new Error(
5424
+ `PRD document-set files must contain at most ${MAX_PRD_DOCUMENT_MARKDOWN_CHARS} characters: ${absPath}`,
5425
+ );
5426
+ }
5427
+ const clientRef = path.relative(process.cwd(), absPath) || path.basename(absPath);
5428
+ return {
5429
+ client_ref: clientRef,
5430
+ filename: path.basename(absPath),
5431
+ title: String(
5432
+ extractMarkdownTitle(sourceText) || path.parse(absPath).name || `Document ${index + 1}`
5433
+ ).trim(),
5434
+ source_markdown: sourceText,
5435
+ };
5436
+ });
5437
+ const parentTitle = String(args.title || documents[0]?.title || "").trim();
5438
+ if (!parentTitle) {
5439
+ throw new Error("A title is required for a PRD document set.");
5440
+ }
5441
+ const documentSetPayload = {
5442
+ title: parentTitle,
5443
+ documents,
5444
+ };
5445
+ const description = String(
5446
+ args.description || args["feedback-text"] || args.feedbackText || ""
5447
+ ).trim();
5448
+ if (description) documentSetPayload.description = description;
5449
+ const priority = firstNonEmptyString(args.priority);
5450
+ const status = firstNonEmptyString(args.status);
5451
+ const tags = parseFeedbackTags(args);
5452
+ if (priority) documentSetPayload.priority = priority;
5453
+ if (status) documentSetPayload.status = status;
5454
+ if (tags) documentSetPayload.tags = tags;
5455
+ payloads = [documentSetPayload];
5456
+ } else {
5457
+ payloads = resolvedFiles.map(({ absPath, sourceText }) =>
5458
+ buildCreatePrdPayload({
5459
+ sourceText,
5460
+ filePath: absPath,
5461
+ includeClientRef: providedPaths.length > 1,
5462
+ })
5463
+ );
5464
+ }
5465
+ } catch (err) {
5466
+ console.error(err?.message || err);
5467
+ process.exit(1);
4661
5468
  }
4662
5469
  }
4663
5470
 
@@ -4674,7 +5481,10 @@ async function runCreatePrd(args) {
4674
5481
  if (clientSessionId) payload.client_session_id = clientSessionId;
4675
5482
  payload = withWriteApproval(args, payload, {
4676
5483
  artifactKind: "markdown",
4677
- targets: [firstNonEmptyString(payload.client_ref, payload.title, "create-prd")],
5484
+ targets: documentSetMode
5485
+ ? payload.documents.map((document) => document.client_ref)
5486
+ : [firstNonEmptyString(payload.client_ref, payload.title, "create-prd")],
5487
+ batchCount: documentSetMode ? 1 : null,
4678
5488
  });
4679
5489
  const idempotencyKey = resolveProjectMutationIdempotencyKey({
4680
5490
  args,
@@ -4707,9 +5517,12 @@ async function runCreatePrd(args) {
4707
5517
  feedback_id: data.feedback_id || null,
4708
5518
  project_id: data.project_id || null,
4709
5519
  title: data.title || null,
4710
- status: data.status || null,
4711
- deterministic: data.deterministic === true,
4712
- },
5520
+ status: data.status || null,
5521
+ deterministic: data.deterministic === true,
5522
+ document_count: data.document_count || null,
5523
+ primary_document_id: data.primary_document_id || null,
5524
+ documents: Array.isArray(data.documents) ? data.documents : [],
5525
+ },
4713
5526
  null,
4714
5527
  2
4715
5528
  )
@@ -4787,10 +5600,386 @@ async function runCreatePrd(args) {
4787
5600
  if (message && status !== "created") line.push(`- ${message}`);
4788
5601
  console.log(line.join(" "));
4789
5602
  }
4790
- if (aggregated.failed_count > 0) process.exitCode = 1;
4791
- }
4792
-
4793
- async function runConfig(args) {
5603
+ if (aggregated.failed_count > 0) process.exitCode = 1;
5604
+ }
5605
+
5606
+ function readJsonFileQuiet(filePath) {
5607
+ try {
5608
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
5609
+ } catch {
5610
+ return null;
5611
+ }
5612
+ }
5613
+
5614
+ function packageRuntimeInfo() {
5615
+ const corePackage = readJsonFileQuiet(path.join(__dirname, "package.json")) || {};
5616
+ let wrapperPackage = null;
5617
+ const localWrapperPath = path.resolve(__dirname, "..", "myte", "package.json");
5618
+ if (fs.existsSync(localWrapperPath)) {
5619
+ wrapperPackage = readJsonFileQuiet(localWrapperPath);
5620
+ }
5621
+ if (!wrapperPackage) {
5622
+ try {
5623
+ wrapperPackage = require("myte/package.json");
5624
+ } catch {
5625
+ wrapperPackage = null;
5626
+ }
5627
+ }
5628
+ const expectedCoreVersion =
5629
+ wrapperPackage?.dependencies?.["@mytegroupinc/myte-core"] || null;
5630
+ const runtimeDependencies = Object.entries(corePackage.dependencies || {})
5631
+ .map(([name, version]) => ({
5632
+ name,
5633
+ version: String(version),
5634
+ }))
5635
+ .sort((left, right) => left.name.localeCompare(right.name));
5636
+ return {
5637
+ wrapper: {
5638
+ name: wrapperPackage?.name || "myte",
5639
+ version: wrapperPackage?.version || null,
5640
+ },
5641
+ core: {
5642
+ name: corePackage?.name || "@mytegroupinc/myte-core",
5643
+ version: corePackage?.version || null,
5644
+ },
5645
+ compatible:
5646
+ !wrapperPackage ||
5647
+ !expectedCoreVersion ||
5648
+ expectedCoreVersion === corePackage?.version,
5649
+ expected_core_version: expectedCoreVersion,
5650
+ runtime_dependencies: runtimeDependencies,
5651
+ install_graph: {
5652
+ package_count: (wrapperPackage ? 2 : 1) + runtimeDependencies.length,
5653
+ explanation: wrapperPackage
5654
+ ? "public wrapper + CLI core + core runtime dependencies"
5655
+ : "CLI core + core runtime dependencies",
5656
+ },
5657
+ };
5658
+ }
5659
+
5660
+ function projectKeySummary(key = getProjectApiKey()) {
5661
+ const normalized = String(key || "").trim();
5662
+ return {
5663
+ present: Boolean(normalized),
5664
+ source: process.env.MYTE_API_KEY
5665
+ ? "MYTE_API_KEY"
5666
+ : process.env.MYTE_PROJECT_API_KEY
5667
+ ? "MYTE_PROJECT_API_KEY"
5668
+ : null,
5669
+ fingerprint: normalized
5670
+ ? createHash("sha256").update(normalized).digest("hex").slice(0, 12)
5671
+ : null,
5672
+ };
5673
+ }
5674
+
5675
+ function probeDns(
5676
+ hostname,
5677
+ timeoutMs,
5678
+ {
5679
+ lookup = dns.promises.lookup.bind(dns.promises),
5680
+ setTimeoutFn = setTimeout,
5681
+ clearTimeoutFn = clearTimeout,
5682
+ } = {},
5683
+ ) {
5684
+ if (net.isIP(hostname)) {
5685
+ return Promise.resolve({
5686
+ ok: true,
5687
+ hostname,
5688
+ addresses: [{ address: hostname, family: net.isIP(hostname) }],
5689
+ });
5690
+ }
5691
+ return new Promise((resolve) => {
5692
+ let settled = false;
5693
+ let timer;
5694
+ const finish = (payload) => {
5695
+ if (settled) return;
5696
+ settled = true;
5697
+ if (timer !== undefined) clearTimeoutFn(timer);
5698
+ resolve(payload);
5699
+ };
5700
+ timer = setTimeoutFn(() => {
5701
+ const error = new Error("DNS probe timed out.");
5702
+ error.code = "ETIMEDOUT";
5703
+ finish({
5704
+ ok: false,
5705
+ hostname,
5706
+ failure: publicRequestFailure(
5707
+ createNetworkRequestError(error, `https://${hostname}`, { timedOut: true }),
5708
+ ),
5709
+ });
5710
+ }, timeoutMs);
5711
+ Promise.resolve()
5712
+ .then(() => lookup(hostname, { all: true }))
5713
+ .then(
5714
+ (addresses) => {
5715
+ finish({
5716
+ ok: true,
5717
+ hostname,
5718
+ addresses: addresses.map((entry) => ({
5719
+ address: entry.address,
5720
+ family: entry.family,
5721
+ })),
5722
+ });
5723
+ },
5724
+ (error) => {
5725
+ finish({
5726
+ ok: false,
5727
+ hostname,
5728
+ failure: publicRequestFailure(
5729
+ createNetworkRequestError(error, `https://${hostname}`),
5730
+ ),
5731
+ });
5732
+ },
5733
+ );
5734
+ });
5735
+ }
5736
+
5737
+ function probeDirectTransport(apiBase, timeoutMs) {
5738
+ const parsed = new URL(apiBase);
5739
+ const secure = parsed.protocol === "https:";
5740
+ const port = Number(parsed.port || (secure ? 443 : 80));
5741
+ const startedAt = Date.now();
5742
+ return new Promise((resolve) => {
5743
+ let settled = false;
5744
+ const finish = (payload) => {
5745
+ if (settled) return;
5746
+ settled = true;
5747
+ clearTimeout(timer);
5748
+ try {
5749
+ socket.destroy();
5750
+ } catch {}
5751
+ resolve({
5752
+ ...payload,
5753
+ protocol: secure ? "tls" : "tcp",
5754
+ host: parsed.hostname,
5755
+ port,
5756
+ duration_ms: Date.now() - startedAt,
5757
+ });
5758
+ };
5759
+ const options = {
5760
+ host: parsed.hostname,
5761
+ port,
5762
+ ...(secure && !net.isIP(parsed.hostname) ? { servername: parsed.hostname } : {}),
5763
+ };
5764
+ const socket = secure ? tls.connect(options) : net.connect(options);
5765
+ const timer = setTimeout(() => {
5766
+ const error = new Error("Transport probe timed out.");
5767
+ error.code = "ETIMEDOUT";
5768
+ finish({
5769
+ ok: false,
5770
+ failure: publicRequestFailure(
5771
+ createNetworkRequestError(error, apiBase, { timedOut: true }),
5772
+ ),
5773
+ });
5774
+ }, timeoutMs);
5775
+ socket.once(secure ? "secureConnect" : "connect", () => {
5776
+ finish({
5777
+ ok: true,
5778
+ authorized: secure ? socket.authorized === true : null,
5779
+ tls_protocol: secure ? socket.getProtocol?.() || null : null,
5780
+ });
5781
+ });
5782
+ socket.once("error", (error) => {
5783
+ finish({
5784
+ ok: false,
5785
+ failure: publicRequestFailure(createNetworkRequestError(error, apiBase)),
5786
+ });
5787
+ });
5788
+ });
5789
+ }
5790
+
5791
+ async function runInfo(args, envPath) {
5792
+ const packages = packageRuntimeInfo();
5793
+ const payload = {
5794
+ command: "myte",
5795
+ packages,
5796
+ node_version: process.version,
5797
+ platform: `${process.platform}-${process.arch}`,
5798
+ api_base: resolveApiBase(args),
5799
+ project_key: projectKeySummary(),
5800
+ proxy: proxyEnvironmentSummary(),
5801
+ workspace: {
5802
+ cwd: process.cwd(),
5803
+ env_file: envPath,
5804
+ },
5805
+ };
5806
+ if (args.json) {
5807
+ console.log(JSON.stringify(payload, null, 2));
5808
+ return;
5809
+ }
5810
+ console.log(`myte ${packages.wrapper.version || packages.core.version || "unknown"}`);
5811
+ console.log(`core: ${packages.core.version || "unknown"}`);
5812
+ console.log(`wrapper/core compatible: ${packages.compatible ? "yes" : "no"}`);
5813
+ console.log(
5814
+ `runtime dependencies: ${
5815
+ packages.runtime_dependencies.length
5816
+ ? packages.runtime_dependencies
5817
+ .map((dependency) => `${dependency.name}@${dependency.version}`)
5818
+ .join(", ")
5819
+ : "none"
5820
+ }`,
5821
+ );
5822
+ console.log(`npm install graph: ${packages.install_graph.package_count} packages`);
5823
+ console.log(`node: ${payload.node_version}`);
5824
+ console.log(`api: ${payload.api_base}`);
5825
+ console.log(`project key: ${payload.project_key.present ? "present" : "missing"}`);
5826
+ console.log(`proxy: ${payload.proxy.configured ? "configured" : "direct"}`);
5827
+ console.log(`workspace: ${payload.workspace.cwd}`);
5828
+ }
5829
+
5830
+ async function runVersion(args) {
5831
+ const packages = packageRuntimeInfo();
5832
+ const version = packages.wrapper.version || packages.core.version || "unknown";
5833
+ if (args.verbose || args.json) {
5834
+ const payload = {
5835
+ version,
5836
+ ...packages,
5837
+ node_version: process.version,
5838
+ compatible: packages.compatible,
5839
+ };
5840
+ if (args.json) {
5841
+ console.log(JSON.stringify(payload, null, 2));
5842
+ } else {
5843
+ console.log(`myte ${version}`);
5844
+ console.log(`core ${packages.core.version || "unknown"}`);
5845
+ console.log(
5846
+ `runtime dependencies: ${
5847
+ packages.runtime_dependencies.length
5848
+ ? packages.runtime_dependencies
5849
+ .map((dependency) => `${dependency.name}@${dependency.version}`)
5850
+ .join(", ")
5851
+ : "none"
5852
+ }`,
5853
+ );
5854
+ console.log(`npm install graph: ${packages.install_graph.package_count} packages`);
5855
+ console.log(`node ${process.version}`);
5856
+ console.log(`wrapper/core compatible: ${packages.compatible ? "yes" : "no"}`);
5857
+ }
5858
+ return;
5859
+ }
5860
+ console.log(version);
5861
+ }
5862
+
5863
+ async function runDoctor(args, envPath) {
5864
+ const timeoutMs = Math.max(
5865
+ 1_000,
5866
+ Math.min(30_000, Number(args["timeout-ms"] || args.timeoutMs || 10_000) || 10_000),
5867
+ );
5868
+ const apiBase = resolveApiBase(args);
5869
+ const parsedBase = new URL(apiBase);
5870
+ const key = getProjectApiKey();
5871
+ const proxy = proxyEnvironmentSummary();
5872
+ const packages = packageRuntimeInfo();
5873
+ const payload = {
5874
+ schema_version: 1,
5875
+ command: "myte doctor",
5876
+ generated_at: new Date().toISOString(),
5877
+ packages,
5878
+ runtime: {
5879
+ node_version: process.version,
5880
+ platform: `${process.platform}-${process.arch}`,
5881
+ transport: null,
5882
+ },
5883
+ api: {
5884
+ base: apiBase,
5885
+ endpoint: parsedBase.host,
5886
+ },
5887
+ project_key: projectKeySummary(key),
5888
+ proxy,
5889
+ dns: null,
5890
+ direct_transport: null,
5891
+ config_probe: null,
5892
+ workspace: {
5893
+ cwd: process.cwd(),
5894
+ env_file: envPath,
5895
+ mode: "unknown",
5896
+ root: process.cwd(),
5897
+ configured_repos: [],
5898
+ found_repos: [],
5899
+ missing_repos: [],
5900
+ },
5901
+ ok: false,
5902
+ failure_layer: null,
5903
+ };
5904
+
5905
+ try {
5906
+ payload.runtime.transport = (await getFetchRuntime()).transport;
5907
+ } catch (error) {
5908
+ payload.runtime.transport = "unavailable";
5909
+ payload.config_probe = {
5910
+ ok: false,
5911
+ failure: publicRequestFailure(error),
5912
+ };
5913
+ }
5914
+ payload.dns = await probeDns(parsedBase.hostname, timeoutMs);
5915
+ payload.direct_transport = await probeDirectTransport(apiBase, timeoutMs);
5916
+
5917
+ if (!payload.config_probe && key) {
5918
+ try {
5919
+ const config = await fetchProjectConfig({ apiBase, key, timeoutMs });
5920
+ const repoNames = Array.isArray(config.repo_names) ? config.repo_names : [];
5921
+ const repoBindings = Array.isArray(config.repo_bindings) ? config.repo_bindings : [];
5922
+ const resolved = repoBindings.length
5923
+ ? resolveConfiguredRepoBindings(repoBindings)
5924
+ : resolvePortableWorkspace(repoNames);
5925
+ payload.config_probe = {
5926
+ ok: true,
5927
+ project_id: config.project_id || null,
5928
+ repo_count: repoNames.length,
5929
+ };
5930
+ payload.workspace = {
5931
+ ...payload.workspace,
5932
+ mode: resolved.mode || "unknown",
5933
+ root: resolved.root || process.cwd(),
5934
+ configured_repos: repoNames,
5935
+ found_repos: (resolved.repos || []).map((repo) => repo.name),
5936
+ missing_repos: resolved.missing || [],
5937
+ };
5938
+ } catch (error) {
5939
+ payload.config_probe = {
5940
+ ok: false,
5941
+ failure: publicRequestFailure(error),
5942
+ };
5943
+ }
5944
+ } else if (!payload.config_probe) {
5945
+ payload.config_probe = {
5946
+ ok: false,
5947
+ skipped: true,
5948
+ reason: "project_key_missing",
5949
+ };
5950
+ }
5951
+
5952
+ payload.ok = payload.config_probe.ok === true;
5953
+ const firstFailure =
5954
+ payload.config_probe.failure ||
5955
+ (!payload.direct_transport.ok ? payload.direct_transport.failure : null) ||
5956
+ (!payload.dns.ok ? payload.dns.failure : null);
5957
+ payload.failure_layer = firstFailure?.layer || null;
5958
+
5959
+ if (args.json) {
5960
+ console.log(JSON.stringify(payload, null, 2));
5961
+ } else {
5962
+ console.log(`Myte doctor: ${payload.ok ? "reachable" : "not ready"}`);
5963
+ console.log(`API: ${payload.api.base}`);
5964
+ console.log(`Key: ${payload.project_key.present ? "present" : "missing"}`);
5965
+ console.log(`Proxy: ${payload.proxy.configured ? "configured" : "direct"}`);
5966
+ console.log(`DNS: ${payload.dns.ok ? "ok" : "failed"}`);
5967
+ console.log(
5968
+ `Direct ${payload.direct_transport.protocol || "transport"}: ${
5969
+ payload.direct_transport.ok ? "ok" : "failed"
5970
+ }`,
5971
+ );
5972
+ console.log(`Authenticated config: ${payload.config_probe.ok ? "ok" : "failed"}`);
5973
+ if (firstFailure) {
5974
+ console.log(`Failure layer: ${firstFailure.layer || "unknown"}`);
5975
+ console.log(`Cause: ${firstFailure.code || "unknown"}`);
5976
+ console.log(`Detail: ${firstFailure.detail || "Request failed."}`);
5977
+ }
5978
+ }
5979
+ if (!payload.ok) process.exitCode = 1;
5980
+ }
5981
+
5982
+ async function runConfig(args) {
4794
5983
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
4795
5984
  if (!key) {
4796
5985
  console.error("Missing MYTE_API_KEY (project key) in environment/.env");
@@ -5074,7 +6263,7 @@ async function runSyncQaqc(args) {
5074
6263
  console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
5075
6264
  }
5076
6265
 
5077
- async function runFeedbackSync(args) {
6266
+ async function runFeedbackSync(args) {
5078
6267
  const key = getProjectApiKey();
5079
6268
  if (!key) {
5080
6269
  console.error("Missing MYTE_API_KEY (project key) in environment/.env");
@@ -5083,17 +6272,45 @@ async function runFeedbackSync(args) {
5083
6272
 
5084
6273
  const timeoutMs = resolveTimeoutMs(args);
5085
6274
  const apiBase = resolveApiBase(args);
5086
- const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
5087
- const filters = {
6275
+ const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
6276
+ let pageSize;
6277
+ let maxItems;
6278
+ try {
6279
+ pageSize = resolveBoundedInteger(
6280
+ args["page-size"] || args.pageSize || args.page_size,
6281
+ { fallback: DEFAULT_FEEDBACK_SYNC_PAGE_SIZE, min: 1, max: 500, label: "page-size" }
6282
+ );
6283
+ maxItems = resolveBoundedInteger(
6284
+ args.limit,
6285
+ { fallback: 0, min: 0, max: 100_000, label: "limit" }
6286
+ );
6287
+ } catch (err) {
6288
+ console.error(err?.message || err);
6289
+ process.exit(1);
6290
+ }
6291
+ const filters = {
5088
6292
  status: firstNonEmptyString(args.status) || "",
5089
6293
  source: firstNonEmptyString(args.source) || "",
5090
6294
  includePrdText,
5091
6295
  includeCommentTurns: true,
5092
6296
  };
5093
6297
 
5094
- let snapshot;
5095
- try {
5096
- snapshot = await fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters });
6298
+ let snapshot;
6299
+ try {
6300
+ snapshot = await fetchCompleteFeedbackSyncSnapshot({
6301
+ apiBase,
6302
+ key,
6303
+ timeoutMs,
6304
+ filters,
6305
+ pageSize,
6306
+ maxItems,
6307
+ onPage: args.json
6308
+ ? undefined
6309
+ : ({ page, received, synced, total }) => {
6310
+ const totalText = Number.isFinite(Number(total)) ? `/${total}` : "";
6311
+ console.error(`Feedback sync page ${page}: +${received} (${synced}${totalText})`);
6312
+ },
6313
+ });
5097
6314
  } catch (err) {
5098
6315
  console.error("Failed to fetch feedback sync snapshot:", err?.message || err);
5099
6316
  process.exit(1);
@@ -5130,8 +6347,10 @@ async function runFeedbackSync(args) {
5130
6347
  filters: snapshot.filters && typeof snapshot.filters === "object" ? snapshot.filters : filters,
5131
6348
  counts,
5132
6349
  snapshot_hash: snapshot.snapshot_hash || null,
5133
- generated_at: snapshot.generated_at || null,
5134
- dry_run: dryRun,
6350
+ generated_at: snapshot.generated_at || null,
6351
+ pagination: snapshot.pagination || null,
6352
+ diagnostics: snapshot.diagnostics || null,
6353
+ dry_run: dryRun,
5135
6354
  };
5136
6355
 
5137
6356
  if (dryRun) {
@@ -5172,22 +6391,23 @@ async function runFeedbackSync(args) {
5172
6391
  console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
5173
6392
  }
5174
6393
 
5175
- async function refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeoutMs }) {
6394
+ async function refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeoutMs }) {
5176
6395
  if (args.sync === false || args["no-sync"] || args.noSync || args.no_sync) {
5177
6396
  return { skipped: true };
5178
6397
  }
5179
6398
  try {
5180
- const snapshot = await fetchFeedbackSyncSnapshot({
5181
- apiBase,
5182
- key,
5183
- timeoutMs,
5184
- filters: {
6399
+ const snapshot = await fetchCompleteFeedbackSyncSnapshot({
6400
+ apiBase,
6401
+ key,
6402
+ timeoutMs,
6403
+ filters: {
5185
6404
  status: "",
5186
6405
  source: "",
5187
6406
  includePrdText: true,
5188
- includeCommentTurns: true,
5189
- },
5190
- });
6407
+ includeCommentTurns: true,
6408
+ },
6409
+ pageSize: DEFAULT_FEEDBACK_SYNC_PAGE_SIZE,
6410
+ });
5191
6411
  const resolved = resolvePortableWorkspace(snapshot.repo_names || []);
5192
6412
  const writeResult = writeFeedbackSnapshot({
5193
6413
  snapshot,
@@ -5270,9 +6490,62 @@ async function buildFeedbackDraftForCommand(args, subcommand) {
5270
6490
  const tags = parseFeedbackTags(args);
5271
6491
  if (tags) changes.tags = feedbackChange(Array.isArray(item.tags) ? item.tags : [], tags);
5272
6492
 
5273
- const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
5274
- if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
5275
- } else {
6493
+ const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
6494
+ if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
6495
+
6496
+ const prdFile = firstNonEmptyString(args["prd-file"], args.prdFile, args.prd_file);
6497
+ if (prdFile) {
6498
+ const proposedPath = resolveInputFile(prdFile, "PRD document");
6499
+ const proposedMarkdown = fs.readFileSync(proposedPath, "utf8");
6500
+ if (!proposedMarkdown.trim()) {
6501
+ console.error("PRD document markdown cannot be empty.");
6502
+ process.exit(1);
6503
+ }
6504
+ if (proposedMarkdown.length > 250000) {
6505
+ console.error("PRD document markdown must be at most 250000 characters.");
6506
+ process.exit(1);
6507
+ }
6508
+
6509
+ const documents = Array.isArray(item.prd_documents) ? item.prd_documents : [];
6510
+ const explicitDocumentId = firstNonEmptyString(
6511
+ args["document-id"],
6512
+ args.documentId,
6513
+ args.document_id
6514
+ );
6515
+ const targetDocumentId = explicitDocumentId
6516
+ || firstNonEmptyString(item.primary_prd_document_id)
6517
+ || firstNonEmptyString(documents[0]?.document_id);
6518
+ const targetDocument = targetDocumentId
6519
+ ? documents.find((document) => String(document?.document_id || "") === targetDocumentId)
6520
+ : null;
6521
+ if (explicitDocumentId && !targetDocument) {
6522
+ console.error(`PRD document not found in feedback.yml: ${explicitDocumentId}. Run \`myte feedback-sync\` first.`);
6523
+ process.exit(1);
6524
+ }
6525
+ if (documents.length > 0 && !targetDocument) {
6526
+ console.error("Unable to resolve the primary PRD document. Run `myte feedback-sync` first.");
6527
+ process.exit(1);
6528
+ }
6529
+
6530
+ let currentMarkdown = "";
6531
+ const localFile = firstNonEmptyString(targetDocument?.local_file, item.prd_file);
6532
+ if (localFile) {
6533
+ const currentPath = path.resolve(paths.targetRoot, localFile);
6534
+ const relativePath = path.relative(paths.targetRoot, currentPath);
6535
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath) && fs.existsSync(currentPath)) {
6536
+ currentMarkdown = fs.readFileSync(currentPath, "utf8");
6537
+ }
6538
+ }
6539
+ changes.prd_markdown = feedbackChange(currentMarkdown, proposedMarkdown);
6540
+ changes.prd_format = feedbackChange(
6541
+ firstNonEmptyString(item.prd_format, "markdown"),
6542
+ "markdown"
6543
+ );
6544
+ if (targetDocumentId) {
6545
+ args["document-id"] = targetDocumentId;
6546
+ }
6547
+ }
6548
+ } else {
5276
6549
  console.error("Unknown feedback draft command. Use status, edit, assign, archive, or refine.");
5277
6550
  process.exit(1);
5278
6551
  }
@@ -5627,7 +6900,7 @@ async function runFeedbackReviews(args) {
5627
6900
  }
5628
6901
  }
5629
6902
 
5630
- function buildFeedbackReviewDecisionPayload(args, action) {
6903
+ function buildFeedbackReviewDecisionPayload(args, action) {
5631
6904
  const filePath = firstNonEmptyString(args.file);
5632
6905
  let payload = {};
5633
6906
  if (filePath) {
@@ -5639,9 +6912,25 @@ function buildFeedbackReviewDecisionPayload(args, action) {
5639
6912
  }
5640
6913
  const nextPayload = { ...payload };
5641
6914
  const reason = firstNonEmptyString(args.reason, args["review-note"], args.reviewNote, args.review_note);
5642
- if (reason && !nextPayload.reason && !nextPayload.review_reason) {
5643
- nextPayload.reason = reason;
5644
- }
6915
+ if (reason && !nextPayload.reason && !nextPayload.review_reason) {
6916
+ nextPayload.reason = reason;
6917
+ }
6918
+ const verificationEvidence = firstNonEmptyString(
6919
+ args["verification-evidence"],
6920
+ args.verificationEvidence,
6921
+ args.verification_evidence,
6922
+ );
6923
+ if (verificationEvidence && !nextPayload.verification_evidence) {
6924
+ nextPayload.verification_evidence = verificationEvidence;
6925
+ }
6926
+ const deploymentEvidence = firstNonEmptyString(
6927
+ args["deployment-evidence"],
6928
+ args.deploymentEvidence,
6929
+ args.deployment_evidence,
6930
+ );
6931
+ if (deploymentEvidence && !nextPayload.deployment_evidence) {
6932
+ nextPayload.deployment_evidence = deploymentEvidence;
6933
+ }
5645
6934
  const finalFile = firstNonEmptyString(args["final-file"], args.finalFile, args.final_file);
5646
6935
  if (finalFile) {
5647
6936
  nextPayload.final_change_set = readStructuredPayloadFile(finalFile, "Feedback final change set");
@@ -5681,12 +6970,24 @@ async function runFeedbackReviewDecision(args) {
5681
6970
  console.error("Missing --request-ids for batch feedback review.");
5682
6971
  process.exit(1);
5683
6972
  }
5684
- const reason = firstNonEmptyString(args.reason, args["review-note"], args.reviewNote, args.review_note);
6973
+ const reason = firstNonEmptyString(args.reason, args["review-note"], args.reviewNote, args.review_note);
6974
+ const verificationEvidence = firstNonEmptyString(
6975
+ args["verification-evidence"],
6976
+ args.verificationEvidence,
6977
+ args.verification_evidence,
6978
+ );
6979
+ const deploymentEvidence = firstNonEmptyString(
6980
+ args["deployment-evidence"],
6981
+ args.deploymentEvidence,
6982
+ args.deployment_evidence,
6983
+ );
5685
6984
  let payload = {
5686
6985
  ...(isPlainObject(filePayload) && !Array.isArray(filePayload) ? filePayload : {}),
5687
6986
  items,
5688
6987
  action,
5689
6988
  reason: reason || undefined,
6989
+ verification_evidence: verificationEvidence || undefined,
6990
+ deployment_evidence: deploymentEvidence || undefined,
5690
6991
  };
5691
6992
  const timeoutMs = resolveTimeoutMs(args);
5692
6993
  const apiBase = resolveApiBase(args);
@@ -6079,12 +7380,21 @@ async function runFeedbackPrdDiff(args) {
6079
7380
  console.error("Missing --version-id.");
6080
7381
  process.exit(1);
6081
7382
  }
6082
- const timeoutMs = resolveTimeoutMs(args);
6083
- const apiBase = resolveApiBase(args);
6084
- const compareTo = firstNonEmptyString(args["compare-to"], args.compareTo, args.compare_to);
6085
- let data;
6086
- try {
6087
- data = await fetchFeedbackPrdVersionDiff({ apiBase, key, timeoutMs, feedbackId, versionId, compareTo });
7383
+ const timeoutMs = resolveTimeoutMs(args);
7384
+ const apiBase = resolveApiBase(args);
7385
+ const compareTo = firstNonEmptyString(args["compare-to"], args.compareTo, args.compare_to);
7386
+ const documentId = firstNonEmptyString(args["document-id"], args.documentId, args.document_id);
7387
+ let data;
7388
+ try {
7389
+ data = await fetchFeedbackPrdVersionDiff({
7390
+ apiBase,
7391
+ key,
7392
+ timeoutMs,
7393
+ feedbackId,
7394
+ versionId,
7395
+ compareTo,
7396
+ documentId,
7397
+ });
6088
7398
  } catch (err) {
6089
7399
  console.error("Feedback PRD diff failed:", err?.message || err);
6090
7400
  process.exit(1);
@@ -6798,8 +8108,9 @@ async function runQuery(args) {
6798
8108
  process.exit(1);
6799
8109
  }
6800
8110
 
6801
- const includeDiff = Boolean(args["with-diff"] || args.withDiff || args.diff || args.d);
6802
- const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
8111
+ const includeDiff = Boolean(args["with-diff"] || args.withDiff || args.diff || args.d);
8112
+ const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
8113
+ const verbose = Boolean(args.verbose || args.v);
6803
8114
 
6804
8115
  const fetchRemote = args.fetch !== undefined ? Boolean(args.fetch) : true;
6805
8116
 
@@ -6871,11 +8182,32 @@ async function runQuery(args) {
6871
8182
  process.exit(0);
6872
8183
  }
6873
8184
 
6874
- let data;
6875
- try {
6876
- const queued = await createAssistantQueryJob({ apiBase, key, payload, timeoutMs });
6877
- if (queued.answer) {
6878
- data = queued;
8185
+ let data;
8186
+ const requestId = queryRequestId(args);
8187
+ try {
8188
+ if (verbose) {
8189
+ const runtime = await getFetchRuntime();
8190
+ console.error(
8191
+ `[myte] query request_id=${requestId} endpoint=${safeEndpointHost(apiBase)} transport=${runtime.transport}`,
8192
+ );
8193
+ }
8194
+ const queued = await withTransientQueryRetries(
8195
+ () =>
8196
+ createAssistantQueryJob({
8197
+ apiBase,
8198
+ key,
8199
+ payload,
8200
+ timeoutMs,
8201
+ requestId,
8202
+ }),
8203
+ {
8204
+ maxAttempts: 3,
8205
+ verbose,
8206
+ label: "query creation",
8207
+ },
8208
+ );
8209
+ if (queued.answer) {
8210
+ data = queued;
6879
8211
  } else {
6880
8212
  const jobId = firstNonEmptyString(queued.job_id, queued.id);
6881
8213
  if (!jobId) {
@@ -6886,21 +8218,24 @@ async function runQuery(args) {
6886
8218
  let finalStatus = null;
6887
8219
  let pollDelayMs = 2_000;
6888
8220
  do {
6889
- await sleep(pollDelayMs);
6890
- try {
6891
- finalStatus = await fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId });
6892
- pollDelayMs = Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
6893
- } catch (err) {
6894
- if (isTransientQueryStatusError(err)) {
6895
- const fallbackMs =
6896
- Number(err?.status) === 429 ? 5_000 : Math.min(30_000, Math.max(5_000, pollDelayMs * 2));
6897
- const waitMs = resolveRetryAfterMs(err, fallbackMs);
6898
- await sleep(waitMs);
6899
- pollDelayMs = Math.min(30_000, Math.max(pollDelayMs, waitMs));
6900
- continue;
6901
- }
6902
- throw err;
6903
- }
8221
+ await sleep(pollDelayMs);
8222
+ finalStatus = await withTransientQueryRetries(
8223
+ () =>
8224
+ fetchAssistantQueryJobStatus({
8225
+ apiBase,
8226
+ key,
8227
+ timeoutMs,
8228
+ jobId,
8229
+ requestId,
8230
+ }),
8231
+ {
8232
+ maxAttempts: 4,
8233
+ verbose,
8234
+ label: "query status",
8235
+ initialDelayMs: Math.min(2_000, pollDelayMs),
8236
+ },
8237
+ );
8238
+ pollDelayMs = Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
6904
8239
  if (["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
6905
8240
  break;
6906
8241
  }
@@ -6913,22 +8248,37 @@ async function runQuery(args) {
6913
8248
  const detail = firstNonEmptyString(finalStatus?.error?.message, finalStatus?.error?.code);
6914
8249
  throw new Error(detail || `Query job ${jobId} failed`);
6915
8250
  }
6916
- data = {
6917
- answer: finalStatus.answer,
6918
- context_blocks: finalStatus.context_blocks,
6919
- telemetry: finalStatus.telemetry,
8251
+ data = {
8252
+ job_id: jobId,
8253
+ answer: finalStatus.answer,
8254
+ context_blocks: finalStatus.context_blocks,
8255
+ telemetry: finalStatus.telemetry,
6920
8256
  };
6921
8257
  }
6922
- } catch (err) {
6923
- if (err?.name === "AbortError") {
6924
- console.error(`Request timed out after ${timeoutMs}ms`);
6925
- } else {
6926
- console.error("Assistant query failed:", err?.message || err);
6927
- }
6928
- process.exit(1);
6929
- }
6930
-
6931
- console.log("Answer:\n", data.answer || "(no answer)");
8258
+ } catch (err) {
8259
+ console.error("Assistant query failed:", err?.message || err);
8260
+ process.exit(1);
8261
+ }
8262
+
8263
+ if (args.json) {
8264
+ console.log(
8265
+ JSON.stringify(
8266
+ {
8267
+ ok: true,
8268
+ request_id: requestId,
8269
+ job_id: firstNonEmptyString(data.job_id, data.id) || null,
8270
+ answer: data.answer || "",
8271
+ context_blocks: Array.isArray(data.context_blocks) ? data.context_blocks : [],
8272
+ telemetry: data.telemetry || null,
8273
+ },
8274
+ null,
8275
+ 2,
8276
+ ),
8277
+ );
8278
+ return;
8279
+ }
8280
+
8281
+ console.log("Answer:\n", data.answer || "(no answer)");
6932
8282
  if (data.context_blocks?.length) console.log(`\nContext blocks: ${data.context_blocks.length}`);
6933
8283
  if (data.telemetry) {
6934
8284
  const t = data.telemetry;
@@ -6942,8 +8292,8 @@ async function runChat(args) {
6942
8292
  process.exit(1);
6943
8293
  }
6944
8294
 
6945
- async function main() {
6946
- loadEnv();
8295
+ async function main() {
8296
+ const envPath = loadEnv();
6947
8297
 
6948
8298
  const { command, rest } = splitCommand(process.argv.slice(2));
6949
8299
  if (REMOVED_COMMAND_MESSAGES[command]) {
@@ -6955,12 +8305,27 @@ async function main() {
6955
8305
  process.exit(1);
6956
8306
  }
6957
8307
  const args = parseArgs(rest);
6958
- if (args.help || command === "help") {
8308
+ if (args.help || command === "help") {
6959
8309
  printHelp();
6960
8310
  return;
6961
- }
6962
-
6963
- if (command === "config") {
8311
+ }
8312
+
8313
+ if (command === "version") {
8314
+ await runVersion(args);
8315
+ return;
8316
+ }
8317
+
8318
+ if (command === "info") {
8319
+ await runInfo(args, envPath);
8320
+ return;
8321
+ }
8322
+
8323
+ if (command === "doctor") {
8324
+ await runDoctor(args, envPath);
8325
+ return;
8326
+ }
8327
+
8328
+ if (command === "config") {
6964
8329
  await runConfig(args);
6965
8330
  return;
6966
8331
  }
@@ -7040,12 +8405,19 @@ if (require.main === module) {
7040
8405
  process.exit(1);
7041
8406
  });
7042
8407
  } else {
7043
- module.exports = {
7044
- buildMissionOpsPayload,
7045
- collectCreateDraftPayload,
8408
+ module.exports = {
8409
+ buildFetchRuntime,
8410
+ buildMissionOpsPayload,
8411
+ classifyNetworkFailure,
8412
+ createHttpResponseError,
8413
+ createNetworkRequestError,
8414
+ collectCreateDraftPayload,
7046
8415
  collectReviewPayload,
7047
8416
  collectRevisionPayload,
7048
- normalizeItemsPayload,
8417
+ normalizeItemsPayload,
8418
+ proxyEnvironmentSummary,
8419
+ probeDns,
8420
+ publicRequestFailure,
7049
8421
  preserveMissionOpsWorkspace,
7050
8422
  pruneLegacyCommandCenterArtifacts,
7051
8423
  readYamlFile,
@@ -7057,6 +8429,8 @@ if (require.main === module) {
7057
8429
  writeFeedbackSnapshot,
7058
8430
  writeApprovedMissionCards,
7059
8431
  writeMissionOpsSnapshot,
7060
- writeQaqcSnapshot,
7061
- };
7062
- }
8432
+ writeQaqcSnapshot,
8433
+ fetchJsonWithTimeout,
8434
+ withTransientQueryRetries,
8435
+ };
8436
+ }