@mytegroupinc/myte-core 0.0.47 → 0.0.50
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/README.md +110 -21
- package/cli.js +1883 -349
- package/lib/certification-manifest.js +178 -0
- package/package.json +8 -5
- package/scripts/feedback-certification-cleanup.js +53 -0
- package/scripts/feedback-live-full-harness.js +1355 -142
- package/scripts/project-assistant-read-certification.js +470 -0
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
|
|
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,15 +123,20 @@ function splitCommand(argv) {
|
|
|
112
123
|
"-h",
|
|
113
124
|
]);
|
|
114
125
|
const first = argv[0];
|
|
115
|
-
if (first && known.has(first)) {
|
|
116
|
-
const cmd =
|
|
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 };
|
|
120
136
|
}
|
|
121
137
|
|
|
122
|
-
function parseArgs(argv) {
|
|
123
|
-
const parsed = { _: []
|
|
138
|
+
function parseArgs(argv) {
|
|
139
|
+
const parsed = { _: [] };
|
|
124
140
|
const aliases = { q: "query", d: "with-diff", c: "context", h: "help" };
|
|
125
141
|
const setValue = (key, value) => {
|
|
126
142
|
const finalKey = aliases[key] || key;
|
|
@@ -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
|
|
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",
|
|
@@ -307,12 +328,14 @@ function printHelp() {
|
|
|
307
328
|
" - Optional: --target-contact-id <id> (repeatable) or --target-contact-ids <id1,id2>",
|
|
308
329
|
" - If the project has no linked client contacts, the backend falls back to the project owner for internal projects",
|
|
309
330
|
"",
|
|
310
|
-
"feedback-sync contract:",
|
|
331
|
+
"feedback-sync contract:",
|
|
311
332
|
" - Runs from any workspace where you want local MyteCommandCenter data written",
|
|
312
333
|
" - Syncs non-archived feedback by default so local Command Center data stays focused on active visible work",
|
|
313
334
|
" - Archived feedback is intentionally excluded from normal sync; returning it to active work is web UI only",
|
|
314
|
-
" - Writes project feedback metadata and
|
|
315
|
-
" - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
|
|
335
|
+
" - Writes project feedback metadata and comment turns, stable comment ids, and nested attachment metadata into MyteCommandCenter/data/feedback.yml",
|
|
336
|
+
" - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
|
|
337
|
+
" - Stores readable text/Markdown/DOCX comment attachment context under MyteCommandCenter/feedback-sync/comment-attachments and never writes raw storage URLs",
|
|
338
|
+
" - Non-readable comment attachments remain nested under the correct comment as metadata_only context",
|
|
316
339
|
" - Reads existing feedback comments and can create text-only feedback-specific comments through the project-key API",
|
|
317
340
|
"",
|
|
318
341
|
"feedback review contract:",
|
|
@@ -327,7 +350,8 @@ function printHelp() {
|
|
|
327
350
|
" - comment creates text-only feedback-specific comments; attachments remain web UI only for this endpoint",
|
|
328
351
|
" - project-key API/CLI cannot unarchive archived Feedback; use the web archived Feedback view",
|
|
329
352
|
" - 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",
|
|
353
|
+
" - prd-versions/prd-diff expose retained PRD versions and backend-generated per-document text diffs",
|
|
354
|
+
" - edit/refine with --prd-file updates a review artifact only; --document-id targets one document in a PRD document set",
|
|
331
355
|
" - validate/apply send artifacts to /api/project-assistant/feedback/<id>/refinement/* for validation or owner-direct apply",
|
|
332
356
|
" - The backend owns authorization, stale snapshot checks, allowed field/transition rules, and history",
|
|
333
357
|
" - apply is idempotent and does not rewrite local feedback.yml; run feedback-sync after apply to refresh local state",
|
|
@@ -364,7 +388,9 @@ function printHelp() {
|
|
|
364
388
|
"Options:",
|
|
365
389
|
" --with-diff Include deterministic git diffs (project-scoped; fails fast if no project repos are configured or resolved)",
|
|
366
390
|
" --diff-limit <chars> Truncate diff context to N chars (default: 500000)",
|
|
367
|
-
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
391
|
+
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
392
|
+
" --verbose Print sanitized transport and retry diagnostics",
|
|
393
|
+
" --request-id <id> Stable, non-secret query request/idempotency identifier",
|
|
368
394
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
369
395
|
" --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
|
|
370
396
|
" --json-response Ask the Myte AI gateway to return clean JSON only and send OpenAI-compatible response_format",
|
|
@@ -373,8 +399,9 @@ function printHelp() {
|
|
|
373
399
|
" --output-dir <path> Command Center output directory (default: <current-workspace>/MyteCommandCenter)",
|
|
374
400
|
" --file <path> YAML/JSON payload file for suggestions create/revise/review",
|
|
375
401
|
" --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",
|
|
402
|
+
" --title <text> Override PRD title for raw markdown uploads",
|
|
403
|
+
" --description <text> Set feedback description/card summary for raw markdown uploads",
|
|
404
|
+
" --document-set Create one PRD feedback item containing one to three ordered markdown files",
|
|
378
405
|
" --content <text> Team update content for update-team",
|
|
379
406
|
" --body <text> Feedback comment body for feedback comment",
|
|
380
407
|
" --subject <text> Subject for update-owner or update-client",
|
|
@@ -387,8 +414,10 @@ function printHelp() {
|
|
|
387
414
|
" --request-id <id> Feedback review request ObjectId for submit/revise/review flows",
|
|
388
415
|
" --request-ids <ids> Comma-separated Feedback review request ObjectIds for batch review",
|
|
389
416
|
" --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",
|
|
417
|
+
" --version-id <id> Feedback PRD version ObjectId for prd-diff",
|
|
418
|
+
" --compare-to <id> Optional base PRD version ObjectId for prd-diff",
|
|
419
|
+
" --document-id <id> Stable PRD document id for document refinement or version diff",
|
|
420
|
+
" --prd-file <path> Read proposed PRD document Markdown into a local feedback review artifact",
|
|
392
421
|
" --action <value> Feedback review action: approve, reject, request_changes, or cancel",
|
|
393
422
|
" --to-state <value> Target canonical feedback board state for feedback move",
|
|
394
423
|
" --from-state <value> Optional current-state guard for feedback move",
|
|
@@ -401,7 +430,8 @@ function printHelp() {
|
|
|
401
430
|
" --due-date <date> Feedback due date target for edit artifacts",
|
|
402
431
|
" --review-note <text> Review note stored in feedback refinement history",
|
|
403
432
|
" --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
|
|
404
|
-
" --no-with-prd-text Skip PRD
|
|
433
|
+
" --no-with-prd-text Skip PRD and readable comment-attachment extraction; keep feedback/comment/attachment metadata",
|
|
434
|
+
" --comment-attachment-limit <n> Cap readable comment-attachment extraction per sync page (0-100; default: 20)",
|
|
405
435
|
" --mission-ids <ids> Comma-separated mission business ids for run-qaqc, mission status, or mission archive (quote multi-id values on PowerShell)",
|
|
406
436
|
" --reason <text> Optional reason for governed mission archive or feedback changes",
|
|
407
437
|
" --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
|
|
@@ -409,13 +439,16 @@ function printHelp() {
|
|
|
409
439
|
" --approval-artifact Local .md/.markdown/.yml/.yaml/.json approval artifact for gated write commands",
|
|
410
440
|
" --wait Poll batch status until terminal completion for run-qaqc",
|
|
411
441
|
" --sync After run-qaqc completes, refresh local QAQC file",
|
|
412
|
-
" --force Allow
|
|
442
|
+
" --force Allow QAQC retry or owner/elevated stale Feedback apply; Feedback force requires a reason",
|
|
413
443
|
" --no-sync Skip automatic post-mutation sync for suggestions, missions, and feedback mutations",
|
|
414
444
|
" --print-context Print JSON payload and exit (no query call)",
|
|
415
445
|
" --no-fetch Don't git fetch origin main/master before diff",
|
|
416
446
|
"",
|
|
417
447
|
"Examples:",
|
|
418
|
-
" myte query \"What changed in logging?\" --with-diff",
|
|
448
|
+
" myte query \"What changed in logging?\" --with-diff",
|
|
449
|
+
" myte doctor --json",
|
|
450
|
+
" myte info --json",
|
|
451
|
+
" myte --version --verbose",
|
|
419
452
|
" myte ai \"Explain what this repository does\"",
|
|
420
453
|
" myte ai \"Return a JSON object with risks and next_steps\" --json-response",
|
|
421
454
|
" myte bootstrap",
|
|
@@ -446,6 +479,7 @@ function printHelp() {
|
|
|
446
479
|
" 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
480
|
" myte create-prd ./drafts/auth-prd.md --description \"Short card summary\" --confirm-write --approval-artifact ./drafts/auth-prd.md",
|
|
448
481
|
" myte create-prd ./drafts/auth-prd.md ./drafts/billing-prd.md --confirm-write --approval-artifact ./drafts/prd-batch.md",
|
|
482
|
+
" 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
483
|
" cat ./drafts/auth-prd.md | myte create-prd --stdin --confirm-write --approval-artifact ./drafts/auth-prd.md",
|
|
450
484
|
" myte config",
|
|
451
485
|
].join("\n");
|
|
@@ -669,14 +703,280 @@ function parseFeedbackRequestIdsArg(args) {
|
|
|
669
703
|
);
|
|
670
704
|
}
|
|
671
705
|
|
|
672
|
-
function sleep(ms) {
|
|
673
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
706
|
+
function sleep(ms) {
|
|
707
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function proxyEnvironmentValue(env, lowerName, upperName) {
|
|
711
|
+
const lowerValue = String(env?.[lowerName] || "").trim();
|
|
712
|
+
if (lowerValue) return lowerValue;
|
|
713
|
+
return String(env?.[upperName] || "").trim();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function proxyEnvironmentSummary(env = process.env) {
|
|
717
|
+
const httpProxy = proxyEnvironmentValue(env, "http_proxy", "HTTP_PROXY");
|
|
718
|
+
const httpsProxy = proxyEnvironmentValue(env, "https_proxy", "HTTPS_PROXY");
|
|
719
|
+
const noProxy = proxyEnvironmentValue(env, "no_proxy", "NO_PROXY");
|
|
720
|
+
return {
|
|
721
|
+
configured: Boolean(httpProxy || httpsProxy),
|
|
722
|
+
http_proxy_present: Boolean(httpProxy),
|
|
723
|
+
https_proxy_present: Boolean(httpsProxy),
|
|
724
|
+
no_proxy_present: Boolean(noProxy),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function buildFetchRuntime({
|
|
729
|
+
env = process.env,
|
|
730
|
+
nativeFetch = globalThis.fetch,
|
|
731
|
+
undiciModule,
|
|
732
|
+
} = {}) {
|
|
733
|
+
const proxySummary = proxyEnvironmentSummary(env);
|
|
734
|
+
if (!proxySummary.configured) {
|
|
735
|
+
if (typeof nativeFetch !== "function") {
|
|
736
|
+
const error = new Error("Global fetch is unavailable. myte requires Node 18.17+.");
|
|
737
|
+
error.code = "MYTE_FETCH_UNAVAILABLE";
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
fetch: nativeFetch,
|
|
742
|
+
transport: "node_fetch",
|
|
743
|
+
proxy: proxySummary,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
let undici = undiciModule;
|
|
748
|
+
if (!undici) {
|
|
749
|
+
try {
|
|
750
|
+
undici = require("undici");
|
|
751
|
+
} catch (cause) {
|
|
752
|
+
const error = new Error(
|
|
753
|
+
"Proxy variables are configured, but the Myte HTTP transport is unavailable. Reinstall the myte package.",
|
|
754
|
+
);
|
|
755
|
+
error.code = "MYTE_PROXY_TRANSPORT_UNAVAILABLE";
|
|
756
|
+
error.cause = cause;
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (typeof undici.fetch !== "function" || typeof undici.EnvHttpProxyAgent !== "function") {
|
|
761
|
+
const error = new Error(
|
|
762
|
+
"The installed Myte HTTP transport does not support HTTP_PROXY, HTTPS_PROXY, and NO_PROXY.",
|
|
763
|
+
);
|
|
764
|
+
error.code = "MYTE_PROXY_TRANSPORT_INCOMPATIBLE";
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const dispatcher = new undici.EnvHttpProxyAgent({
|
|
769
|
+
httpProxy: proxyEnvironmentValue(env, "http_proxy", "HTTP_PROXY") || undefined,
|
|
770
|
+
httpsProxy: proxyEnvironmentValue(env, "https_proxy", "HTTPS_PROXY") || undefined,
|
|
771
|
+
noProxy: proxyEnvironmentValue(env, "no_proxy", "NO_PROXY") || undefined,
|
|
772
|
+
});
|
|
773
|
+
return {
|
|
774
|
+
fetch: (url, options = {}) => undici.fetch(url, { ...options, dispatcher }),
|
|
775
|
+
transport: "undici_env_proxy",
|
|
776
|
+
proxy: proxySummary,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
let fetchRuntimePromise = null;
|
|
781
|
+
|
|
782
|
+
async function getFetchRuntime() {
|
|
783
|
+
if (!fetchRuntimePromise) {
|
|
784
|
+
fetchRuntimePromise = Promise.resolve().then(() => buildFetchRuntime());
|
|
785
|
+
}
|
|
786
|
+
return fetchRuntimePromise;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function getFetch() {
|
|
790
|
+
return (await getFetchRuntime()).fetch;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function requestErrorChain(error) {
|
|
794
|
+
const chain = [];
|
|
795
|
+
const seen = new Set();
|
|
796
|
+
let current = error;
|
|
797
|
+
while (current && typeof current === "object" && !seen.has(current) && chain.length < 8) {
|
|
798
|
+
seen.add(current);
|
|
799
|
+
chain.push(current);
|
|
800
|
+
current = current.cause;
|
|
801
|
+
}
|
|
802
|
+
return chain;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function requestErrorCode(error) {
|
|
806
|
+
for (const item of requestErrorChain(error)) {
|
|
807
|
+
const code = String(item?.code || item?.errno || "").trim();
|
|
808
|
+
if (code) return code;
|
|
809
|
+
}
|
|
810
|
+
return "";
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function requestErrorMessage(error) {
|
|
814
|
+
return requestErrorChain(error)
|
|
815
|
+
.map((item) => String(item?.message || "").trim())
|
|
816
|
+
.filter(Boolean)
|
|
817
|
+
.join(" ");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function safeEndpointHost(url) {
|
|
821
|
+
try {
|
|
822
|
+
return new URL(String(url || "")).host || "unknown";
|
|
823
|
+
} catch {
|
|
824
|
+
return "unknown";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function classifyNetworkFailure(error, { timedOut = false } = {}) {
|
|
829
|
+
const code = timedOut ? "ETIMEDOUT" : requestErrorCode(error) || "NETWORK_ERROR";
|
|
830
|
+
const message = requestErrorMessage(error).toLowerCase();
|
|
831
|
+
const certificateFailure =
|
|
832
|
+
/^(CERT_|ERR_TLS_CERT_|DEPTH_ZERO_SELF_SIGNED_CERT|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE)/.test(code) ||
|
|
833
|
+
/certificate|self[- ]signed|unable to verify|hostname\/ip does not match/.test(message);
|
|
834
|
+
|
|
835
|
+
if (timedOut || code === "ABORT_ERR" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ETIMEDOUT") {
|
|
836
|
+
return {
|
|
837
|
+
kind: "timeout",
|
|
838
|
+
layer: "network",
|
|
839
|
+
code,
|
|
840
|
+
detail: "The request exceeded its configured timeout before a complete HTTP response was received.",
|
|
841
|
+
guidance: "Retry on a stable network or increase --timeout-ms when the API is known to be reachable.",
|
|
842
|
+
transient: true,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
846
|
+
return {
|
|
847
|
+
kind: "dns",
|
|
848
|
+
layer: "dns",
|
|
849
|
+
code,
|
|
850
|
+
detail: "The API hostname could not be resolved.",
|
|
851
|
+
guidance: "Check DNS connectivity, captive-portal login, VPN, or local resolver settings.",
|
|
852
|
+
transient: true,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
if (code === "ECONNREFUSED") {
|
|
856
|
+
return {
|
|
857
|
+
kind: "connection_refused",
|
|
858
|
+
layer: "tcp",
|
|
859
|
+
code,
|
|
860
|
+
detail: "The remote host refused the TCP connection.",
|
|
861
|
+
guidance: "Check the API base, proxy/VPN path, firewall policy, and service availability.",
|
|
862
|
+
transient: true,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
if (certificateFailure) {
|
|
866
|
+
return {
|
|
867
|
+
kind: "tls_certificate",
|
|
868
|
+
layer: "tls",
|
|
869
|
+
code,
|
|
870
|
+
detail: "TLS certificate validation failed.",
|
|
871
|
+
guidance: "Check system time, trusted corporate certificates, proxy interception, and the requested hostname. Do not disable TLS verification.",
|
|
872
|
+
transient: false,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
if (
|
|
876
|
+
code === "ECONNRESET" ||
|
|
877
|
+
code === "UND_ERR_SOCKET" ||
|
|
878
|
+
/secure tls connection|tls connection|socket disconnected|connection reset|other side closed/.test(message)
|
|
879
|
+
) {
|
|
880
|
+
return {
|
|
881
|
+
kind: "connection_reset",
|
|
882
|
+
layer: "tls",
|
|
883
|
+
code,
|
|
884
|
+
detail: /before secure tls|secure tls connection/.test(message)
|
|
885
|
+
? "The TLS connection was reset before the secure connection was established."
|
|
886
|
+
: "The network connection was reset before a complete HTTP response was received.",
|
|
887
|
+
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.",
|
|
888
|
+
transient: true,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
if (/ssl|tls|handshake/.test(message) || code.startsWith("ERR_SSL")) {
|
|
892
|
+
return {
|
|
893
|
+
kind: "tls",
|
|
894
|
+
layer: "tls",
|
|
895
|
+
code,
|
|
896
|
+
detail: "The TLS handshake failed before an HTTP response was received.",
|
|
897
|
+
guidance: "Check captive-portal login, proxy/VPN interception, system trust, or retry on another network.",
|
|
898
|
+
transient: true,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
kind: "network",
|
|
903
|
+
layer: "network",
|
|
904
|
+
code,
|
|
905
|
+
detail: "The transport failed before a complete HTTP response was received.",
|
|
906
|
+
guidance: "Run `myte doctor --json`, then check network, proxy/VPN, captive-portal, and API base settings.",
|
|
907
|
+
transient: true,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function createNetworkRequestError(error, url, { timedOut = false } = {}) {
|
|
912
|
+
if (error?.isMyteRequestError) return error;
|
|
913
|
+
const classified = classifyNetworkFailure(error, { timedOut });
|
|
914
|
+
const endpoint = safeEndpointHost(url);
|
|
915
|
+
const wrapped = new Error(
|
|
916
|
+
[
|
|
917
|
+
"Network request failed before receiving an HTTP response.",
|
|
918
|
+
`Endpoint: ${endpoint}`,
|
|
919
|
+
`Cause: ${classified.code}`,
|
|
920
|
+
`Detail: ${classified.detail}`,
|
|
921
|
+
classified.guidance,
|
|
922
|
+
].join("\n"),
|
|
923
|
+
);
|
|
924
|
+
wrapped.name = "MyteNetworkError";
|
|
925
|
+
wrapped.code = classified.code;
|
|
926
|
+
wrapped.failureKind = classified.kind;
|
|
927
|
+
wrapped.failureLayer = classified.layer;
|
|
928
|
+
wrapped.endpoint = endpoint;
|
|
929
|
+
wrapped.detail = classified.detail;
|
|
930
|
+
wrapped.guidance = classified.guidance;
|
|
931
|
+
wrapped.transient = classified.transient;
|
|
932
|
+
wrapped.isMyteRequestError = true;
|
|
933
|
+
wrapped.cause = error;
|
|
934
|
+
return wrapped;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function createNonJsonResponseError(resp, url) {
|
|
938
|
+
const status = Number(resp?.status || 0);
|
|
939
|
+
const contentType = String(resp?.headers?.get?.("content-type") || "").toLowerCase();
|
|
940
|
+
const endpoint = safeEndpointHost(url);
|
|
941
|
+
const likelyPortal = status >= 200 && status < 400 && contentType.includes("text/html");
|
|
942
|
+
const detail = likelyPortal
|
|
943
|
+
? "The endpoint returned HTML instead of JSON, which commonly indicates a captive portal or intercepting proxy."
|
|
944
|
+
: "The endpoint returned a non-JSON response, so the CLI could not validate the API contract.";
|
|
945
|
+
const error = new Error(
|
|
946
|
+
[
|
|
947
|
+
`Invalid HTTP response (${status || "unknown status"}).`,
|
|
948
|
+
`Endpoint: ${endpoint}`,
|
|
949
|
+
`Cause: ${likelyPortal ? "CAPTIVE_PORTAL_OR_PROXY" : "NON_JSON_RESPONSE"}`,
|
|
950
|
+
`Detail: ${detail}`,
|
|
951
|
+
].join("\n"),
|
|
952
|
+
);
|
|
953
|
+
error.name = "MyteHttpError";
|
|
954
|
+
error.code = likelyPortal ? "CAPTIVE_PORTAL_OR_PROXY" : "NON_JSON_RESPONSE";
|
|
955
|
+
error.failureKind = likelyPortal ? "captive_portal" : "non_json_response";
|
|
956
|
+
error.failureLayer = "http";
|
|
957
|
+
error.endpoint = endpoint;
|
|
958
|
+
error.status = status;
|
|
959
|
+
error.transient = likelyPortal || [408, 429, 500, 502, 503, 504].includes(status);
|
|
960
|
+
error.isMyteRequestError = true;
|
|
961
|
+
const retryAfter = resp?.headers?.get?.("retry-after");
|
|
962
|
+
const requestId = resp?.headers?.get?.("x-request-id");
|
|
963
|
+
if (retryAfter) error.retryAfter = retryAfter;
|
|
964
|
+
if (requestId) error.requestId = String(requestId);
|
|
965
|
+
return error;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function publicRequestFailure(error) {
|
|
969
|
+
return {
|
|
970
|
+
layer: error?.failureLayer || (error?.status ? "http" : "unknown"),
|
|
971
|
+
kind: error?.failureKind || "unknown",
|
|
972
|
+
code: String(error?.code || error?.status || "UNKNOWN"),
|
|
973
|
+
status: Number(error?.status || 0) || null,
|
|
974
|
+
endpoint: error?.endpoint || null,
|
|
975
|
+
detail: error?.detail || String(error?.message || "Request failed.").split("\n")[0],
|
|
976
|
+
transient: Boolean(error?.transient),
|
|
977
|
+
request_id: error?.requestId || null,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
680
980
|
|
|
681
981
|
function normalizeApiBase(baseRaw) {
|
|
682
982
|
const baseTrim = String(baseRaw || "").trim().replace(/\/+$/, "");
|
|
@@ -684,24 +984,31 @@ function normalizeApiBase(baseRaw) {
|
|
|
684
984
|
return base.endsWith("/api") ? base : `${base}/api`;
|
|
685
985
|
}
|
|
686
986
|
|
|
687
|
-
async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
|
|
688
|
-
const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
throw
|
|
703
|
-
}
|
|
704
|
-
|
|
987
|
+
async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
|
|
988
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
|
|
989
|
+
let timedOut = false;
|
|
990
|
+
const timeoutId =
|
|
991
|
+
controller && timeoutMs > 0
|
|
992
|
+
? setTimeout(() => {
|
|
993
|
+
timedOut = true;
|
|
994
|
+
controller.abort();
|
|
995
|
+
}, timeoutMs)
|
|
996
|
+
: undefined;
|
|
997
|
+
try {
|
|
998
|
+
let resp;
|
|
999
|
+
try {
|
|
1000
|
+
resp = await fetchFn(url, { ...options, signal: controller?.signal });
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
throw createNetworkRequestError(error, url, { timedOut });
|
|
1003
|
+
}
|
|
1004
|
+
const text = await resp.text();
|
|
1005
|
+
let body;
|
|
1006
|
+
try {
|
|
1007
|
+
body = JSON.parse(text);
|
|
1008
|
+
} catch {
|
|
1009
|
+
throw createNonJsonResponseError(resp, url);
|
|
1010
|
+
}
|
|
1011
|
+
return { resp, body };
|
|
705
1012
|
} finally {
|
|
706
1013
|
if (timeoutId) clearTimeout(timeoutId);
|
|
707
1014
|
}
|
|
@@ -1575,18 +1882,18 @@ function collectGitDiffWithDiagnostics({
|
|
|
1575
1882
|
|
|
1576
1883
|
const headingParts = [`${name} (${dir || "."})`];
|
|
1577
1884
|
if (repo.role) headingParts.push(`role=${repo.role}`);
|
|
1578
|
-
let section = `### ${headingParts.join(" | ")}\n\n`;
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1586
|
-
if (
|
|
1587
|
-
if (
|
|
1588
|
-
|
|
1589
|
-
if (
|
|
1885
|
+
let section = `### ${headingParts.join(" | ")}\n\n`;
|
|
1886
|
+
if (repoSummary.matched_by?.length) {
|
|
1887
|
+
section += `# ?? Matched by ${repoSummary.matched_by.join(", ")}\n`;
|
|
1888
|
+
}
|
|
1889
|
+
if (repoSummary.compare_remote_url) {
|
|
1890
|
+
section += `# ?? Compare remote ${repoSummary.compare_remote_url}\n`;
|
|
1891
|
+
}
|
|
1892
|
+
if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
|
|
1893
|
+
if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
|
|
1894
|
+
if (untrackedDiffs) section += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
|
|
1895
|
+
section += `# ?? Base vs HEAD (${repoSummary.base_ref_label}...HEAD | head=${headBranch})\n`;
|
|
1896
|
+
if (baseDiff) section += `${baseDiff}\n\n`;
|
|
1590
1897
|
|
|
1591
1898
|
if (!repoSummary.has_changes) {
|
|
1592
1899
|
section += "_No local changes - working tree clean_\n\n";
|
|
@@ -1635,14 +1942,11 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
|
1635
1942
|
headers: { Authorization: `Bearer ${key}` },
|
|
1636
1943
|
},
|
|
1637
1944
|
timeoutMs
|
|
1638
|
-
);
|
|
1639
|
-
|
|
1640
|
-
if (!resp.ok || body.status !== "success") {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
err.status = resp.status;
|
|
1644
|
-
throw err;
|
|
1645
|
-
}
|
|
1945
|
+
);
|
|
1946
|
+
|
|
1947
|
+
if (!resp.ok || body.status !== "success") {
|
|
1948
|
+
throw createHttpResponseError(resp, body, url, "Project configuration could not be loaded.");
|
|
1949
|
+
}
|
|
1646
1950
|
return body.data || {};
|
|
1647
1951
|
}
|
|
1648
1952
|
|
|
@@ -1696,7 +2000,7 @@ async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
|
|
|
1696
2000
|
return body.data || {};
|
|
1697
2001
|
}
|
|
1698
2002
|
|
|
1699
|
-
async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {} }) {
|
|
2003
|
+
async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {} }) {
|
|
1700
2004
|
const fetchFn = await getFetch();
|
|
1701
2005
|
const url = new URL(`${apiBase}/project-assistant/feedback-sync`);
|
|
1702
2006
|
if (filters.status) url.searchParams.set("status", String(filters.status));
|
|
@@ -1704,9 +2008,36 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
|
|
|
1704
2008
|
if (filters.includePrdText !== undefined) {
|
|
1705
2009
|
url.searchParams.set("include_prd_text", filters.includePrdText ? "true" : "false");
|
|
1706
2010
|
}
|
|
1707
|
-
if (filters.includeCommentTurns !== undefined) {
|
|
1708
|
-
url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
|
|
1709
|
-
}
|
|
2011
|
+
if (filters.includeCommentTurns !== undefined) {
|
|
2012
|
+
url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
|
|
2013
|
+
}
|
|
2014
|
+
if (filters.includeCommentAttachmentText !== undefined) {
|
|
2015
|
+
url.searchParams.set(
|
|
2016
|
+
"include_comment_attachment_text",
|
|
2017
|
+
filters.includeCommentAttachmentText ? "true" : "false"
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
if (
|
|
2021
|
+
filters.commentAttachmentTextLimit !== undefined
|
|
2022
|
+
&& filters.commentAttachmentTextLimit !== null
|
|
2023
|
+
) {
|
|
2024
|
+
url.searchParams.set(
|
|
2025
|
+
"comment_attachment_text_limit",
|
|
2026
|
+
String(filters.commentAttachmentTextLimit)
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
if (filters.limit !== undefined && filters.limit !== null && filters.limit !== "") {
|
|
2030
|
+
url.searchParams.set("limit", String(filters.limit));
|
|
2031
|
+
}
|
|
2032
|
+
if (filters.offset !== undefined && filters.offset !== null && Number(filters.offset) > 0) {
|
|
2033
|
+
url.searchParams.set("offset", String(filters.offset));
|
|
2034
|
+
}
|
|
2035
|
+
if (filters.paginationMode) {
|
|
2036
|
+
url.searchParams.set("pagination_mode", String(filters.paginationMode));
|
|
2037
|
+
}
|
|
2038
|
+
if (filters.cursor) {
|
|
2039
|
+
url.searchParams.set("cursor", String(filters.cursor));
|
|
2040
|
+
}
|
|
1710
2041
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
1711
2042
|
fetchFn,
|
|
1712
2043
|
url.toString(),
|
|
@@ -1723,8 +2054,212 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
|
|
|
1723
2054
|
err.status = resp.status;
|
|
1724
2055
|
throw err;
|
|
1725
2056
|
}
|
|
1726
|
-
return body.data || {};
|
|
1727
|
-
}
|
|
2057
|
+
return body.data || {};
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function resolveBoundedInteger(value, { fallback, min, max, label }) {
|
|
2061
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
2062
|
+
const parsed = Number(value);
|
|
2063
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
2064
|
+
throw new Error(`${label} must be an integer between ${min} and ${max}.`);
|
|
2065
|
+
}
|
|
2066
|
+
return parsed;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function aggregateFeedbackSyncCounts(items, availableTotal) {
|
|
2070
|
+
const byStatus = {};
|
|
2071
|
+
const bySource = {};
|
|
2072
|
+
let withPrdText = 0;
|
|
2073
|
+
let withConversationTurns = 0;
|
|
2074
|
+
let withCommentAttachments = 0;
|
|
2075
|
+
let commentAttachments = 0;
|
|
2076
|
+
const commentAttachmentContext = {};
|
|
2077
|
+
for (const item of items) {
|
|
2078
|
+
const status = String(item?.status || "Pending");
|
|
2079
|
+
const source = String(item?.source || "User");
|
|
2080
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
2081
|
+
bySource[source] = (bySource[source] || 0) + 1;
|
|
2082
|
+
if (String(item?.prd_text || "").trim()) withPrdText += 1;
|
|
2083
|
+
if (Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0) {
|
|
2084
|
+
withConversationTurns += 1;
|
|
2085
|
+
}
|
|
2086
|
+
const itemCommentAttachments = (item?.conversation_turns || []).reduce(
|
|
2087
|
+
(total, turn) => total + (Array.isArray(turn?.attachments) ? turn.attachments.length : 0),
|
|
2088
|
+
0
|
|
2089
|
+
);
|
|
2090
|
+
if (itemCommentAttachments > 0) {
|
|
2091
|
+
withCommentAttachments += 1;
|
|
2092
|
+
commentAttachments += itemCommentAttachments;
|
|
2093
|
+
for (const turn of item?.conversation_turns || []) {
|
|
2094
|
+
for (const attachment of turn?.attachments || []) {
|
|
2095
|
+
const contextStatus = String(attachment?.context_status || "metadata_only");
|
|
2096
|
+
commentAttachmentContext[contextStatus] =
|
|
2097
|
+
(commentAttachmentContext[contextStatus] || 0) + 1;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
return {
|
|
2103
|
+
total_feedback: items.length,
|
|
2104
|
+
available_feedback: Number.isFinite(Number(availableTotal)) ? Number(availableTotal) : items.length,
|
|
2105
|
+
with_prd_text: withPrdText,
|
|
2106
|
+
with_conversation_turns: withConversationTurns,
|
|
2107
|
+
with_comment_attachments: withCommentAttachments,
|
|
2108
|
+
comment_attachments: commentAttachments,
|
|
2109
|
+
comment_attachment_context: commentAttachmentContext,
|
|
2110
|
+
by_status: byStatus,
|
|
2111
|
+
by_source: bySource,
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
async function fetchCompleteFeedbackSyncSnapshot({
|
|
2116
|
+
apiBase,
|
|
2117
|
+
key,
|
|
2118
|
+
timeoutMs,
|
|
2119
|
+
filters = {},
|
|
2120
|
+
pageSize = DEFAULT_FEEDBACK_SYNC_PAGE_SIZE,
|
|
2121
|
+
maxItems = 0,
|
|
2122
|
+
onPage,
|
|
2123
|
+
}) {
|
|
2124
|
+
const items = [];
|
|
2125
|
+
const pageHashes = [];
|
|
2126
|
+
const stageTotals = {};
|
|
2127
|
+
let firstSnapshot = null;
|
|
2128
|
+
let offset = 0;
|
|
2129
|
+
let availableTotal = null;
|
|
2130
|
+
let pageCount = 0;
|
|
2131
|
+
let sawPagination = false;
|
|
2132
|
+
let cursor = null;
|
|
2133
|
+
let useCursorPagination = true;
|
|
2134
|
+
let serverHasMore = false;
|
|
2135
|
+
const seenFeedbackIds = new Set();
|
|
2136
|
+
|
|
2137
|
+
while (true) {
|
|
2138
|
+
const remaining = maxItems > 0 ? maxItems - items.length : pageSize;
|
|
2139
|
+
if (maxItems > 0 && remaining <= 0) break;
|
|
2140
|
+
const requestedLimit = Math.min(pageSize, maxItems > 0 ? remaining : pageSize);
|
|
2141
|
+
const page = await fetchFeedbackSyncSnapshot({
|
|
2142
|
+
apiBase,
|
|
2143
|
+
key,
|
|
2144
|
+
timeoutMs,
|
|
2145
|
+
filters: {
|
|
2146
|
+
...filters,
|
|
2147
|
+
limit: requestedLimit,
|
|
2148
|
+
...(useCursorPagination
|
|
2149
|
+
? {
|
|
2150
|
+
paginationMode: "cursor",
|
|
2151
|
+
...(cursor ? { cursor } : {}),
|
|
2152
|
+
}
|
|
2153
|
+
: { offset }),
|
|
2154
|
+
},
|
|
2155
|
+
});
|
|
2156
|
+
pageCount += 1;
|
|
2157
|
+
if (!firstSnapshot) firstSnapshot = page;
|
|
2158
|
+
|
|
2159
|
+
const pageItems = Array.isArray(page.items) ? page.items : [];
|
|
2160
|
+
const pagination = page.pagination && typeof page.pagination === "object" ? page.pagination : null;
|
|
2161
|
+
sawPagination = sawPagination || Boolean(pagination);
|
|
2162
|
+
if (
|
|
2163
|
+
availableTotal === null &&
|
|
2164
|
+
pagination &&
|
|
2165
|
+
Number.isFinite(Number(pagination.total))
|
|
2166
|
+
) {
|
|
2167
|
+
availableTotal = Number(pagination.total);
|
|
2168
|
+
}
|
|
2169
|
+
if (page.snapshot_hash) pageHashes.push(String(page.snapshot_hash));
|
|
2170
|
+
const stages = page?.diagnostics?.stages_ms;
|
|
2171
|
+
if (stages && typeof stages === "object") {
|
|
2172
|
+
for (const [name, value] of Object.entries(stages)) {
|
|
2173
|
+
const duration = Number(value);
|
|
2174
|
+
if (Number.isFinite(duration)) stageTotals[name] = (stageTotals[name] || 0) + duration;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
for (const item of pageItems) {
|
|
2179
|
+
const feedbackId = firstNonEmptyString(item?.feedback_id, item?._id);
|
|
2180
|
+
if (feedbackId && seenFeedbackIds.has(feedbackId)) {
|
|
2181
|
+
throw new Error(
|
|
2182
|
+
`Feedback sync snapshot changed while paging; duplicate feedback ${feedbackId} was returned. Retry the sync.`,
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
if (feedbackId) seenFeedbackIds.add(feedbackId);
|
|
2186
|
+
items.push(item);
|
|
2187
|
+
}
|
|
2188
|
+
if (typeof onPage === "function") {
|
|
2189
|
+
onPage({
|
|
2190
|
+
page: pageCount,
|
|
2191
|
+
received: pageItems.length,
|
|
2192
|
+
synced: items.length,
|
|
2193
|
+
total: availableTotal,
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
if (!pagination) break;
|
|
2198
|
+
const responseUsesCursor = String(pagination.mode || "").toLowerCase() === "cursor";
|
|
2199
|
+
if (!responseUsesCursor) useCursorPagination = false;
|
|
2200
|
+
const hasMore = Boolean(pagination.has_more);
|
|
2201
|
+
serverHasMore = hasMore;
|
|
2202
|
+
if (!hasMore) break;
|
|
2203
|
+
if (pageItems.length === 0) {
|
|
2204
|
+
throw new Error(`Feedback sync page ${pageCount} reported more results but returned no items.`);
|
|
2205
|
+
}
|
|
2206
|
+
if (responseUsesCursor) {
|
|
2207
|
+
const nextCursor = firstNonEmptyString(pagination.next_cursor);
|
|
2208
|
+
if (!nextCursor || nextCursor === cursor) {
|
|
2209
|
+
throw new Error(
|
|
2210
|
+
`Feedback sync page ${pageCount} did not return a new cursor while more results remain.`,
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
cursor = nextCursor;
|
|
2214
|
+
useCursorPagination = true;
|
|
2215
|
+
} else {
|
|
2216
|
+
useCursorPagination = false;
|
|
2217
|
+
offset += pageItems.length;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const base = firstSnapshot || {};
|
|
2222
|
+
if (!sawPagination && pageCount === 1) {
|
|
2223
|
+
return {
|
|
2224
|
+
...base,
|
|
2225
|
+
diagnostics: {
|
|
2226
|
+
...(base.diagnostics && typeof base.diagnostics === "object" ? base.diagnostics : {}),
|
|
2227
|
+
page_count: 1,
|
|
2228
|
+
},
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
const aggregateHash = createHash("sha256")
|
|
2232
|
+
.update(JSON.stringify({
|
|
2233
|
+
project_id: base?.project?.id || null,
|
|
2234
|
+
item_hashes: items.map((item) => item?.snapshot_hash || item?.feedback_id || null),
|
|
2235
|
+
page_hashes: pageHashes,
|
|
2236
|
+
}))
|
|
2237
|
+
.digest("hex");
|
|
2238
|
+
|
|
2239
|
+
return {
|
|
2240
|
+
...base,
|
|
2241
|
+
items,
|
|
2242
|
+
counts: aggregateFeedbackSyncCounts(items, availableTotal),
|
|
2243
|
+
pagination: {
|
|
2244
|
+
mode: useCursorPagination && sawPagination ? "cursor" : "offset",
|
|
2245
|
+
total: availableTotal ?? items.length,
|
|
2246
|
+
limit: pageSize,
|
|
2247
|
+
offset: 0,
|
|
2248
|
+
has_more:
|
|
2249
|
+
maxItems > 0
|
|
2250
|
+
? serverHasMore || (availableTotal !== null && items.length < availableTotal)
|
|
2251
|
+
: false,
|
|
2252
|
+
page_count: pageCount,
|
|
2253
|
+
synced_count: items.length,
|
|
2254
|
+
next_cursor: maxItems > 0 && serverHasMore ? cursor : null,
|
|
2255
|
+
},
|
|
2256
|
+
snapshot_hash: aggregateHash,
|
|
2257
|
+
diagnostics: {
|
|
2258
|
+
page_count: pageCount,
|
|
2259
|
+
stages_ms: stageTotals,
|
|
2260
|
+
},
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
1728
2263
|
|
|
1729
2264
|
async function fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId }) {
|
|
1730
2265
|
const fetchFn = await getFetch();
|
|
@@ -2069,10 +2604,19 @@ async function fetchFeedbackPrdVersions({ apiBase, key, timeoutMs, feedbackId })
|
|
|
2069
2604
|
return body.data || {};
|
|
2070
2605
|
}
|
|
2071
2606
|
|
|
2072
|
-
async function fetchFeedbackPrdVersionDiff({
|
|
2607
|
+
async function fetchFeedbackPrdVersionDiff({
|
|
2608
|
+
apiBase,
|
|
2609
|
+
key,
|
|
2610
|
+
timeoutMs,
|
|
2611
|
+
feedbackId,
|
|
2612
|
+
versionId,
|
|
2613
|
+
compareTo,
|
|
2614
|
+
documentId,
|
|
2615
|
+
}) {
|
|
2073
2616
|
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));
|
|
2617
|
+
const url = new URL(`${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/prd/versions/${encodeURIComponent(String(versionId || ""))}/diff`);
|
|
2618
|
+
if (compareTo) url.searchParams.set("compare_to", String(compareTo));
|
|
2619
|
+
if (documentId) url.searchParams.set("document_id", String(documentId));
|
|
2076
2620
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
2077
2621
|
fetchFn,
|
|
2078
2622
|
url.toString(),
|
|
@@ -2284,48 +2828,162 @@ async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
|
|
|
2284
2828
|
return body.data || {};
|
|
2285
2829
|
}
|
|
2286
2830
|
|
|
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
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
return
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2831
|
+
function resolveRetryAfterMs(err, fallbackMs = 5_000) {
|
|
2832
|
+
const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
|
|
2833
|
+
const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
|
|
2834
|
+
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
|
|
2835
|
+
return Math.min(retryAfterSeconds * 1_000, 60_000);
|
|
2836
|
+
}
|
|
2837
|
+
const retryAfterDate = Date.parse(String(retryAfterRaw || ""));
|
|
2838
|
+
if (Number.isFinite(retryAfterDate)) {
|
|
2839
|
+
return Math.min(Math.max(0, retryAfterDate - Date.now()), 60_000);
|
|
2840
|
+
}
|
|
2841
|
+
return fallbackMs;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function isTransientQueryStatusError(err) {
|
|
2845
|
+
return Boolean(
|
|
2846
|
+
err?.transient ||
|
|
2847
|
+
["dns", "tcp", "tls", "network"].includes(String(err?.failureLayer || "")) ||
|
|
2848
|
+
[408, 429, 500, 502, 503, 504].includes(Number(err?.status)),
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
function createHttpResponseError(resp, body, url, fallbackMessage) {
|
|
2853
|
+
const status = Number(resp?.status || 0);
|
|
2854
|
+
const endpoint = safeEndpointHost(url);
|
|
2855
|
+
let kind = "http";
|
|
2856
|
+
let detail = fallbackMessage || `HTTP request failed (${status || "unknown status"}).`;
|
|
2857
|
+
let transient = false;
|
|
2858
|
+
if (status === 401) {
|
|
2859
|
+
kind = "authentication";
|
|
2860
|
+
detail = "The API rejected the project key.";
|
|
2861
|
+
} else if (status === 403) {
|
|
2862
|
+
kind = "authorization";
|
|
2863
|
+
detail = "The authenticated project identity is not allowed to perform this action.";
|
|
2864
|
+
} else if (status === 429) {
|
|
2865
|
+
kind = "rate_limit";
|
|
2866
|
+
detail = "The API rate limit is temporarily exhausted.";
|
|
2867
|
+
transient = true;
|
|
2868
|
+
} else if ([408, 500, 502, 503, 504].includes(status)) {
|
|
2869
|
+
kind = "server";
|
|
2870
|
+
detail = "The API or an upstream gateway returned a transient error.";
|
|
2871
|
+
transient = true;
|
|
2872
|
+
}
|
|
2873
|
+
const requestId =
|
|
2874
|
+
resp?.headers?.get?.("x-request-id") ||
|
|
2875
|
+
(body && typeof body === "object" ? body.request_id || body?.data?.request_id : null);
|
|
2876
|
+
const error = new Error(
|
|
2877
|
+
[
|
|
2878
|
+
`HTTP request failed (${status || "unknown status"}).`,
|
|
2879
|
+
`Endpoint: ${endpoint}`,
|
|
2880
|
+
`Cause: ${kind.toUpperCase()}`,
|
|
2881
|
+
`Detail: ${detail}`,
|
|
2882
|
+
requestId ? `Request ID: ${requestId}` : "",
|
|
2883
|
+
]
|
|
2884
|
+
.filter(Boolean)
|
|
2885
|
+
.join("\n"),
|
|
2886
|
+
);
|
|
2887
|
+
error.name = "MyteHttpError";
|
|
2888
|
+
error.code = `HTTP_${status || "ERROR"}`;
|
|
2889
|
+
error.status = status;
|
|
2890
|
+
error.failureKind = kind;
|
|
2891
|
+
error.failureLayer = "http";
|
|
2892
|
+
error.endpoint = endpoint;
|
|
2893
|
+
error.detail = detail;
|
|
2894
|
+
error.transient = transient;
|
|
2895
|
+
error.isMyteRequestError = true;
|
|
2896
|
+
if (requestId) error.requestId = String(requestId);
|
|
2897
|
+
const retryAfter = resp?.headers?.get?.("retry-after");
|
|
2898
|
+
if (retryAfter) error.retryAfter = retryAfter;
|
|
2899
|
+
return error;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
function queryRequestId(args) {
|
|
2903
|
+
const explicit = firstNonEmptyString(
|
|
2904
|
+
args["request-id"],
|
|
2905
|
+
args.requestId,
|
|
2906
|
+
args.request_id,
|
|
2907
|
+
args["idempotency-key"],
|
|
2908
|
+
args.idempotencyKey,
|
|
2909
|
+
args.idempotency_key,
|
|
2910
|
+
);
|
|
2911
|
+
const value = explicit || `myte-query-${randomUUID()}`;
|
|
2912
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
|
|
2913
|
+
throw new Error("request-id must contain 1-128 safe letters, numbers, dots, underscores, colons, or hyphens.");
|
|
2914
|
+
}
|
|
2915
|
+
return value;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
function transientRetryDelayMs(error, attempt, initialDelayMs = 750) {
|
|
2919
|
+
const exponential = Math.min(15_000, initialDelayMs * 2 ** Math.max(0, attempt - 1));
|
|
2920
|
+
const fallback = resolveRetryAfterMs(error, exponential);
|
|
2921
|
+
const jitterLimit = Math.max(0, Number(process.env.MYTE_RETRY_JITTER_MS ?? 250) || 0);
|
|
2922
|
+
const jitter = jitterLimit ? Math.floor(Math.random() * (jitterLimit + 1)) : 0;
|
|
2923
|
+
return Math.min(60_000, fallback + jitter);
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
async function withTransientQueryRetries(
|
|
2927
|
+
operation,
|
|
2928
|
+
{
|
|
2929
|
+
maxAttempts = 3,
|
|
2930
|
+
verbose = false,
|
|
2931
|
+
label = "request",
|
|
2932
|
+
initialDelayMs = 750,
|
|
2933
|
+
} = {},
|
|
2934
|
+
) {
|
|
2935
|
+
let lastError;
|
|
2936
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2937
|
+
try {
|
|
2938
|
+
return await operation(attempt);
|
|
2939
|
+
} catch (error) {
|
|
2940
|
+
lastError = error;
|
|
2941
|
+
if (!isTransientQueryStatusError(error) || attempt >= maxAttempts) throw error;
|
|
2942
|
+
const waitMs = transientRetryDelayMs(error, attempt, initialDelayMs);
|
|
2943
|
+
if (verbose) {
|
|
2944
|
+
console.error(
|
|
2945
|
+
`[myte] ${label} transient failure (${error?.code || error?.status || "unknown"}); retry ${attempt + 1}/${maxAttempts} in ${waitMs}ms.`,
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
await sleep(waitMs);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
throw lastError;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
async function createAssistantQueryJob({
|
|
2955
|
+
apiBase,
|
|
2956
|
+
key,
|
|
2957
|
+
payload,
|
|
2958
|
+
timeoutMs,
|
|
2959
|
+
requestId,
|
|
2960
|
+
endpoint = "/project-assistant/query",
|
|
2961
|
+
}) {
|
|
2962
|
+
const fetchFn = await getFetch();
|
|
2963
|
+
const url = `${apiBase}${endpoint}`;
|
|
2964
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
2304
2965
|
fetchFn,
|
|
2305
2966
|
url,
|
|
2306
2967
|
{
|
|
2307
2968
|
method: "POST",
|
|
2308
2969
|
headers: {
|
|
2309
|
-
"Content-Type": "application/json",
|
|
2310
|
-
Authorization: `Bearer ${key}`,
|
|
2311
|
-
|
|
2312
|
-
|
|
2970
|
+
"Content-Type": "application/json",
|
|
2971
|
+
Authorization: `Bearer ${key}`,
|
|
2972
|
+
"X-Idempotency-Key": requestId,
|
|
2973
|
+
"X-Request-ID": requestId,
|
|
2974
|
+
},
|
|
2975
|
+
body: JSON.stringify({ ...payload, client_request_id: requestId }),
|
|
2313
2976
|
},
|
|
2314
2977
|
timeoutMs
|
|
2315
2978
|
);
|
|
2316
2979
|
|
|
2317
|
-
if (!resp.ok || body.status !== "success") {
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
}
|
|
2325
|
-
return body.data || {};
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId }) {
|
|
2980
|
+
if (!resp.ok || body.status !== "success") {
|
|
2981
|
+
throw createHttpResponseError(resp, body, url, "The query job could not be created.");
|
|
2982
|
+
}
|
|
2983
|
+
return body.data || {};
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId, requestId }) {
|
|
2329
2987
|
const fetchFn = await getFetch();
|
|
2330
2988
|
const url = `${apiBase}/project-assistant/query/${encodeURIComponent(String(jobId || ""))}`;
|
|
2331
2989
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
@@ -2333,19 +2991,17 @@ async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId })
|
|
|
2333
2991
|
url,
|
|
2334
2992
|
{
|
|
2335
2993
|
method: "GET",
|
|
2336
|
-
headers: {
|
|
2994
|
+
headers: {
|
|
2995
|
+
Authorization: `Bearer ${key}`,
|
|
2996
|
+
...(requestId ? { "X-Request-ID": requestId } : {}),
|
|
2997
|
+
},
|
|
2337
2998
|
},
|
|
2338
2999
|
timeoutMs
|
|
2339
3000
|
);
|
|
2340
3001
|
|
|
2341
|
-
if (!resp.ok || body.status !== "success") {
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
-
}
|
|
3002
|
+
if (!resp.ok || body.status !== "success") {
|
|
3003
|
+
throw createHttpResponseError(resp, body, url, "The query status could not be loaded.");
|
|
3004
|
+
}
|
|
2349
3005
|
return body.data || {};
|
|
2350
3006
|
}
|
|
2351
3007
|
|
|
@@ -3278,12 +3934,50 @@ function writeJsonFile(filePath, value) {
|
|
|
3278
3934
|
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
3279
3935
|
}
|
|
3280
3936
|
|
|
3281
|
-
function writeTextFile(filePath, value) {
|
|
3282
|
-
ensureDir(path.dirname(filePath));
|
|
3283
|
-
fs.writeFileSync(filePath, String(value || ""), "utf8");
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
|
-
function
|
|
3937
|
+
function writeTextFile(filePath, value) {
|
|
3938
|
+
ensureDir(path.dirname(filePath));
|
|
3939
|
+
fs.writeFileSync(filePath, String(value || ""), "utf8");
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
function replacePathsTransaction(entries, token) {
|
|
3943
|
+
const prepared = [];
|
|
3944
|
+
try {
|
|
3945
|
+
for (const entry of entries) {
|
|
3946
|
+
if (!entry?.staged || !entry?.target || !fs.existsSync(entry.staged)) {
|
|
3947
|
+
throw new Error(`Missing staged sync artifact for ${entry?.target || "unknown target"}.`);
|
|
3948
|
+
}
|
|
3949
|
+
ensureDir(path.dirname(entry.target));
|
|
3950
|
+
const backup = `${entry.target}.myte-backup-${token}`;
|
|
3951
|
+
if (fs.existsSync(backup)) removePathIfExists(backup);
|
|
3952
|
+
if (fs.existsSync(entry.target)) fs.renameSync(entry.target, backup);
|
|
3953
|
+
prepared.push({ ...entry, backup, installed: false });
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
for (const entry of prepared) {
|
|
3957
|
+
fs.renameSync(entry.staged, entry.target);
|
|
3958
|
+
entry.installed = true;
|
|
3959
|
+
}
|
|
3960
|
+
} catch (error) {
|
|
3961
|
+
for (const entry of [...prepared].reverse()) {
|
|
3962
|
+
if (entry.installed && fs.existsSync(entry.target)) removePathIfExists(entry.target);
|
|
3963
|
+
if (fs.existsSync(entry.backup)) fs.renameSync(entry.backup, entry.target);
|
|
3964
|
+
}
|
|
3965
|
+
throw error;
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
for (const entry of prepared) {
|
|
3969
|
+
if (!fs.existsSync(entry.backup)) continue;
|
|
3970
|
+
try {
|
|
3971
|
+
removePathIfExists(entry.backup);
|
|
3972
|
+
} catch (error) {
|
|
3973
|
+
console.warn(
|
|
3974
|
+
`[myte] Sync committed, but an old backup could not be removed: ${path.basename(entry.backup)}`,
|
|
3975
|
+
);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
function sanitizeFileSegment(value, fallback = "item") {
|
|
3287
3981
|
const cleaned = String(value || "")
|
|
3288
3982
|
.trim()
|
|
3289
3983
|
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
|
|
@@ -3304,22 +3998,47 @@ function ensureTrailingNewline(value) {
|
|
|
3304
3998
|
return text.endsWith("\n") ? text : `${text}\n`;
|
|
3305
3999
|
}
|
|
3306
4000
|
|
|
3307
|
-
function normalizeFeedbackConversationTurns(turns) {
|
|
3308
|
-
if (!Array.isArray(turns)) return [];
|
|
3309
|
-
return turns
|
|
3310
|
-
.map((turn) => {
|
|
3311
|
-
const content = String(turn?.content || "").trim();
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
}
|
|
4001
|
+
function normalizeFeedbackConversationTurns(turns) {
|
|
4002
|
+
if (!Array.isArray(turns)) return [];
|
|
4003
|
+
return turns
|
|
4004
|
+
.map((turn) => {
|
|
4005
|
+
const content = String(turn?.content || "").trim();
|
|
4006
|
+
const attachments = (Array.isArray(turn?.attachments) ? turn.attachments : [])
|
|
4007
|
+
.map((attachment) => {
|
|
4008
|
+
if (!isPlainObject(attachment)) return null;
|
|
4009
|
+
const normalizedAttachment = {
|
|
4010
|
+
name: firstNonEmptyString(attachment.name) || "attachment",
|
|
4011
|
+
context_status: firstNonEmptyString(attachment.context_status) || "metadata_only",
|
|
4012
|
+
};
|
|
4013
|
+
for (const field of ["type", "asset_kind", "download_name", "format"]) {
|
|
4014
|
+
const value = firstNonEmptyString(attachment[field]);
|
|
4015
|
+
if (value) normalizedAttachment[field] = value;
|
|
4016
|
+
}
|
|
4017
|
+
const size = Number(attachment.size);
|
|
4018
|
+
if (Number.isFinite(size) && size >= 0) normalizedAttachment.size = size;
|
|
4019
|
+
const attachmentContent = String(attachment.content || "");
|
|
4020
|
+
if (attachmentContent.trim()) normalizedAttachment.content = attachmentContent;
|
|
4021
|
+
if (attachment.truncated !== undefined) {
|
|
4022
|
+
normalizedAttachment.truncated = Boolean(attachment.truncated);
|
|
4023
|
+
}
|
|
4024
|
+
return normalizedAttachment;
|
|
4025
|
+
})
|
|
4026
|
+
.filter(Boolean);
|
|
4027
|
+
if (!content && attachments.length === 0) return null;
|
|
4028
|
+
const normalized = {
|
|
4029
|
+
comment_id: firstNonEmptyString(turn?.comment_id, turn?.id),
|
|
4030
|
+
sender_name: firstNonEmptyString(turn?.sender_name, turn?.user_name, turn?.author_name) || "Unknown",
|
|
4031
|
+
content,
|
|
4032
|
+
attachments,
|
|
4033
|
+
};
|
|
4034
|
+
const createdAt = firstNonEmptyString(turn?.created_at, turn?.timestamp);
|
|
4035
|
+
if (createdAt) normalized.created_at = createdAt;
|
|
4036
|
+
const updatedAt = firstNonEmptyString(turn?.updated_at);
|
|
4037
|
+
if (updatedAt) normalized.updated_at = updatedAt;
|
|
4038
|
+
return normalized;
|
|
4039
|
+
})
|
|
4040
|
+
.filter(Boolean);
|
|
4041
|
+
}
|
|
3323
4042
|
|
|
3324
4043
|
function readJsonFile(filePath) {
|
|
3325
4044
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
|
|
@@ -3678,37 +4397,149 @@ function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
3678
4397
|
};
|
|
3679
4398
|
}
|
|
3680
4399
|
|
|
3681
|
-
function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
3682
|
-
const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
|
|
3683
|
-
const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
const items = Array.isArray(snapshot.items) ? snapshot.items : [];
|
|
3695
|
-
let prdFileCount = 0;
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
const
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
4400
|
+
function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
4401
|
+
const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
|
|
4402
|
+
const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
|
|
4403
|
+
const commentAttachmentsDir = path.join(targetRoot, "feedback-sync", "comment-attachments");
|
|
4404
|
+
const transactionToken = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
4405
|
+
const stagingRoot = path.join(targetRoot, `.myte-feedback-sync-${transactionToken}`);
|
|
4406
|
+
const stagedDataRoot = path.join(stagingRoot, "data");
|
|
4407
|
+
const stagedPrdRoot = path.join(stagingRoot, "PRD", "feedback-sync");
|
|
4408
|
+
const stagedCommentAttachmentsRoot = path.join(stagingRoot, "feedback-sync", "comment-attachments");
|
|
4409
|
+
ensureDir(stagedDataRoot);
|
|
4410
|
+
ensureDir(stagedPrdRoot);
|
|
4411
|
+
ensureDir(stagedCommentAttachmentsRoot);
|
|
4412
|
+
|
|
4413
|
+
const items = Array.isArray(snapshot.items) ? snapshot.items : [];
|
|
4414
|
+
let prdFileCount = 0;
|
|
4415
|
+
let prdDocumentSetCount = 0;
|
|
4416
|
+
let commentAttachmentCount = 0;
|
|
4417
|
+
let commentAttachmentFileCount = 0;
|
|
4418
|
+
const commentAttachmentContextCounts = {};
|
|
4419
|
+
const materializedItems = items.map((rawItem, index) => {
|
|
4420
|
+
const item = isPlainObject(rawItem) ? { ...rawItem } : {};
|
|
4421
|
+
const feedbackId = stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`);
|
|
4422
|
+
const feedbackFolderName = sanitizeFileSegment(feedbackId, `feedback-${index + 1}`);
|
|
4423
|
+
const conversationTurns = normalizeFeedbackConversationTurns(item.conversation_turns)
|
|
4424
|
+
.map((turn, turnIndex) => {
|
|
4425
|
+
const commentId = stableItemId(
|
|
4426
|
+
turn,
|
|
4427
|
+
["comment_id", "id"],
|
|
4428
|
+
`comment-${String(turnIndex + 1).padStart(3, "0")}`
|
|
4429
|
+
);
|
|
4430
|
+
const commentFolderName = sanitizeFileSegment(commentId, `comment-${turnIndex + 1}`);
|
|
4431
|
+
const attachments = (turn.attachments || []).map((rawAttachment, attachmentIndex) => {
|
|
4432
|
+
const attachment = isPlainObject(rawAttachment) ? { ...rawAttachment } : {};
|
|
4433
|
+
const content = String(attachment.content || "");
|
|
4434
|
+
let localFile = null;
|
|
4435
|
+
commentAttachmentCount += 1;
|
|
4436
|
+
const contextStatus = String(attachment.context_status || "metadata_only");
|
|
4437
|
+
commentAttachmentContextCounts[contextStatus] =
|
|
4438
|
+
(commentAttachmentContextCounts[contextStatus] || 0) + 1;
|
|
4439
|
+
if (content.trim()) {
|
|
4440
|
+
const sourceName = sanitizeFileSegment(
|
|
4441
|
+
attachment.name,
|
|
4442
|
+
`attachment-${attachmentIndex + 1}`
|
|
4443
|
+
);
|
|
4444
|
+
const sourceBase = path.parse(sourceName).name || `attachment-${attachmentIndex + 1}`;
|
|
4445
|
+
const extension = String(attachment.format || "").toLowerCase() === "markdown"
|
|
4446
|
+
? "md"
|
|
4447
|
+
: "txt";
|
|
4448
|
+
const filename = `${String(attachmentIndex + 1).padStart(2, "0")}-${sourceBase}.${extension}`;
|
|
4449
|
+
const stagedPath = path.join(
|
|
4450
|
+
stagedCommentAttachmentsRoot,
|
|
4451
|
+
feedbackFolderName,
|
|
4452
|
+
commentFolderName,
|
|
4453
|
+
filename
|
|
4454
|
+
);
|
|
4455
|
+
ensureDir(path.dirname(stagedPath));
|
|
4456
|
+
writeTextFile(stagedPath, ensureTrailingNewline(content));
|
|
4457
|
+
localFile = toPosixRelativePath(
|
|
4458
|
+
targetRoot,
|
|
4459
|
+
path.join(
|
|
4460
|
+
commentAttachmentsDir,
|
|
4461
|
+
feedbackFolderName,
|
|
4462
|
+
commentFolderName,
|
|
4463
|
+
filename
|
|
4464
|
+
)
|
|
4465
|
+
);
|
|
4466
|
+
commentAttachmentFileCount += 1;
|
|
4467
|
+
}
|
|
4468
|
+
delete attachment.content;
|
|
4469
|
+
return {
|
|
4470
|
+
...attachment,
|
|
4471
|
+
local_file: localFile,
|
|
4472
|
+
};
|
|
4473
|
+
});
|
|
4474
|
+
return {
|
|
4475
|
+
...turn,
|
|
4476
|
+
comment_id: commentId,
|
|
4477
|
+
attachments,
|
|
4478
|
+
};
|
|
4479
|
+
});
|
|
4480
|
+
const prdText = String(item.prd_text || "").trim();
|
|
4481
|
+
const rawPrdDocuments = Array.isArray(item.prd_documents) ? item.prd_documents : [];
|
|
4482
|
+
const attachmentDocuments = Array.isArray(item.attachment_documents) ? item.attachment_documents : [];
|
|
4483
|
+
|
|
4484
|
+
let contextSource = "description_only";
|
|
4485
|
+
let contextNote = "No separate PRD. Use feedback_text as the context for this feedback item.";
|
|
4486
|
+
let prdFile = null;
|
|
4487
|
+
let materializedPrdDocuments = rawPrdDocuments.map((document) =>
|
|
4488
|
+
isPlainObject(document) ? { ...document } : {}
|
|
4489
|
+
);
|
|
4490
|
+
|
|
4491
|
+
if (rawPrdDocuments.length > 0) {
|
|
4492
|
+
prdDocumentSetCount += 1;
|
|
4493
|
+
materializedPrdDocuments = rawPrdDocuments.map((rawDocument, documentIndex) => {
|
|
4494
|
+
const document = isPlainObject(rawDocument) ? { ...rawDocument } : {};
|
|
4495
|
+
const markdown = String(
|
|
4496
|
+
document.canonical_markdown || document.source_markdown || ""
|
|
4497
|
+
);
|
|
4498
|
+
let localFile = null;
|
|
4499
|
+
if (markdown.trim()) {
|
|
4500
|
+
const rawFilename = sanitizeFileSegment(
|
|
4501
|
+
document.filename || document.title,
|
|
4502
|
+
`document-${documentIndex + 1}.md`
|
|
4503
|
+
);
|
|
4504
|
+
const filename = /\.(md|markdown)$/i.test(rawFilename)
|
|
4505
|
+
? rawFilename
|
|
4506
|
+
: `${rawFilename}.md`;
|
|
4507
|
+
const position = Number.isInteger(Number(document.position))
|
|
4508
|
+
? Number(document.position) + 1
|
|
4509
|
+
: documentIndex + 1;
|
|
4510
|
+
const orderedFilename = `${String(position).padStart(2, "0")}-${filename}`;
|
|
4511
|
+
const stagedPath = path.join(stagedPrdRoot, feedbackFolderName, orderedFilename);
|
|
4512
|
+
ensureDir(path.dirname(stagedPath));
|
|
4513
|
+
writeTextFile(stagedPath, ensureTrailingNewline(markdown));
|
|
4514
|
+
localFile = toPosixRelativePath(
|
|
4515
|
+
targetRoot,
|
|
4516
|
+
path.join(prdSyncDir, feedbackFolderName, orderedFilename)
|
|
4517
|
+
);
|
|
4518
|
+
prdFileCount += 1;
|
|
4519
|
+
}
|
|
4520
|
+
return {
|
|
4521
|
+
...document,
|
|
4522
|
+
local_file: localFile,
|
|
4523
|
+
};
|
|
4524
|
+
});
|
|
4525
|
+
const primaryDocumentId = String(item.primary_prd_document_id || "");
|
|
4526
|
+
const primaryDocument =
|
|
4527
|
+
materializedPrdDocuments.find(
|
|
4528
|
+
(document) => String(document.document_id || "") === primaryDocumentId
|
|
4529
|
+
) || materializedPrdDocuments[0];
|
|
4530
|
+
prdFile = primaryDocument?.local_file || null;
|
|
4531
|
+
if (prdFile) {
|
|
4532
|
+
contextSource = "prd_document_set";
|
|
4533
|
+
contextNote = "Readable PRD documents are stored in the linked document files.";
|
|
4534
|
+
} else {
|
|
4535
|
+
contextSource = "prd_declared_but_unavailable";
|
|
4536
|
+
contextNote = "A PRD document set exists, but readable document text was not included in this sync snapshot.";
|
|
4537
|
+
}
|
|
4538
|
+
} else if (prdText) {
|
|
4539
|
+
const prdFilename = `${sanitizeFileSegment(feedbackId, `feedback-${index + 1}`)}.md`;
|
|
4540
|
+
const prdPath = path.join(stagedPrdRoot, prdFilename);
|
|
4541
|
+
writeTextFile(prdPath, ensureTrailingNewline(prdText));
|
|
4542
|
+
prdFile = toPosixRelativePath(targetRoot, path.join(prdSyncDir, prdFilename));
|
|
3712
4543
|
contextSource = attachmentDocuments.length > 0 ? "attachment_file" : "prd_file";
|
|
3713
4544
|
contextNote = attachmentDocuments.length > 0
|
|
3714
4545
|
? "Readable feedback attachment documents are stored in the linked file."
|
|
@@ -3720,31 +4551,45 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
3720
4551
|
}
|
|
3721
4552
|
|
|
3722
4553
|
return {
|
|
3723
|
-
...item,
|
|
3724
|
-
feedback_id: feedbackId,
|
|
3725
|
-
conversation_turns: conversationTurns,
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
4554
|
+
...item,
|
|
4555
|
+
feedback_id: feedbackId,
|
|
4556
|
+
conversation_turns: conversationTurns,
|
|
4557
|
+
prd_documents: materializedPrdDocuments,
|
|
4558
|
+
context_source: contextSource,
|
|
4559
|
+
context_note: contextNote,
|
|
4560
|
+
prd_file: prdFile,
|
|
3729
4561
|
};
|
|
3730
4562
|
});
|
|
3731
4563
|
const snapshotCounts = snapshot.counts && typeof snapshot.counts === "object"
|
|
3732
4564
|
? { ...snapshot.counts }
|
|
3733
4565
|
: { total_feedback: materializedItems.length };
|
|
3734
|
-
snapshotCounts.with_prd_files = prdFileCount;
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
4566
|
+
snapshotCounts.with_prd_files = prdFileCount;
|
|
4567
|
+
snapshotCounts.with_prd_document_sets = prdDocumentSetCount;
|
|
4568
|
+
snapshotCounts.comment_attachments = commentAttachmentCount;
|
|
4569
|
+
snapshotCounts.comment_attachment_context = commentAttachmentContextCounts;
|
|
4570
|
+
snapshotCounts.readable_comment_attachment_files = commentAttachmentFileCount;
|
|
4571
|
+
if (snapshotCounts.with_conversation_turns === undefined) {
|
|
4572
|
+
snapshotCounts.with_conversation_turns = materializedItems.filter((item) => Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0).length;
|
|
4573
|
+
}
|
|
4574
|
+
snapshotCounts.with_comment_attachments = materializedItems.filter(
|
|
4575
|
+
(item) => (item?.conversation_turns || []).some(
|
|
4576
|
+
(turn) => Array.isArray(turn?.attachments) && turn.attachments.length > 0
|
|
4577
|
+
)
|
|
4578
|
+
).length;
|
|
3738
4579
|
|
|
3739
|
-
const payload = scrubBootstrapValue({
|
|
4580
|
+
const payload = scrubBootstrapValue({
|
|
3740
4581
|
schema_version: snapshot.schema_version || 2,
|
|
3741
4582
|
project: snapshot.project || null,
|
|
3742
4583
|
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
3743
4584
|
filters: snapshot.filters && typeof snapshot.filters === "object" ? snapshot.filters : {},
|
|
3744
4585
|
counts: snapshotCounts,
|
|
3745
|
-
artifacts: {
|
|
3746
|
-
feedback_prd_root: "PRD/feedback-sync",
|
|
3747
|
-
|
|
4586
|
+
artifacts: {
|
|
4587
|
+
feedback_prd_root: "PRD/feedback-sync",
|
|
4588
|
+
feedback_comment_attachment_root: "feedback-sync/comment-attachments",
|
|
4589
|
+
with_prd_files: prdFileCount,
|
|
4590
|
+
with_prd_document_sets: prdDocumentSetCount,
|
|
4591
|
+
comment_attachments: commentAttachmentCount,
|
|
4592
|
+
readable_comment_attachment_files: commentAttachmentFileCount,
|
|
3748
4593
|
},
|
|
3749
4594
|
queue: materializedItems
|
|
3750
4595
|
.filter((item) => String(item?.status || "").trim().toLowerCase() !== "resolved")
|
|
@@ -3754,24 +4599,60 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
3754
4599
|
priority: item?.priority || null,
|
|
3755
4600
|
title: item?.title || null,
|
|
3756
4601
|
})),
|
|
3757
|
-
items: materializedItems.map((item) => {
|
|
3758
|
-
const nextItem = { ...item };
|
|
3759
|
-
delete nextItem.prd_text;
|
|
3760
|
-
|
|
3761
|
-
|
|
4602
|
+
items: materializedItems.map((item) => {
|
|
4603
|
+
const nextItem = { ...item };
|
|
4604
|
+
delete nextItem.prd_text;
|
|
4605
|
+
nextItem.prd_documents = (nextItem.prd_documents || []).map((rawDocument) => {
|
|
4606
|
+
const document = isPlainObject(rawDocument) ? { ...rawDocument } : {};
|
|
4607
|
+
delete document.source_markdown;
|
|
4608
|
+
delete document.canonical_markdown;
|
|
4609
|
+
return document;
|
|
4610
|
+
});
|
|
4611
|
+
return nextItem;
|
|
4612
|
+
}),
|
|
3762
4613
|
pagination: snapshot.pagination && typeof snapshot.pagination === "object" ? snapshot.pagination : undefined,
|
|
3763
4614
|
generated_at: snapshot.generated_at || null,
|
|
3764
|
-
snapshot_hash: snapshot.snapshot_hash || null,
|
|
3765
|
-
});
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
}
|
|
4615
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
4616
|
+
});
|
|
4617
|
+
try {
|
|
4618
|
+
const replacements = [];
|
|
4619
|
+
if (snapshot.project && typeof snapshot.project === "object") {
|
|
4620
|
+
const stagedProjectPath = path.join(stagedDataRoot, "project.yml");
|
|
4621
|
+
writeYamlFile(stagedProjectPath, scrubBootstrapValue(snapshot.project));
|
|
4622
|
+
replacements.push({
|
|
4623
|
+
staged: stagedProjectPath,
|
|
4624
|
+
target: path.join(dataRoot, "project.yml"),
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
4627
|
+
const stagedFeedbackPath = path.join(stagedDataRoot, "feedback.yml");
|
|
4628
|
+
writeYamlFile(stagedFeedbackPath, payload);
|
|
4629
|
+
replacements.push(
|
|
4630
|
+
{
|
|
4631
|
+
staged: stagedPrdRoot,
|
|
4632
|
+
target: prdSyncDir,
|
|
4633
|
+
},
|
|
4634
|
+
{
|
|
4635
|
+
staged: stagedCommentAttachmentsRoot,
|
|
4636
|
+
target: commentAttachmentsDir,
|
|
4637
|
+
},
|
|
4638
|
+
{
|
|
4639
|
+
staged: stagedFeedbackPath,
|
|
4640
|
+
target: path.join(dataRoot, "feedback.yml"),
|
|
4641
|
+
}
|
|
4642
|
+
);
|
|
4643
|
+
replacePathsTransaction(replacements, transactionToken);
|
|
4644
|
+
pruneLegacyCommandCenterArtifacts(dataRoot, { bootstrap: true, feedback: true });
|
|
4645
|
+
return {
|
|
4646
|
+
targetRoot,
|
|
4647
|
+
dataRoot,
|
|
4648
|
+
prdSyncDir,
|
|
4649
|
+
commentAttachmentsDir,
|
|
4650
|
+
manifest: payload,
|
|
4651
|
+
};
|
|
4652
|
+
} finally {
|
|
4653
|
+
if (fs.existsSync(stagingRoot)) removePathIfExists(stagingRoot);
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
3775
4656
|
|
|
3776
4657
|
function resolveFeedbackReviewPaths(args, { requireFeedback = false } = {}) {
|
|
3777
4658
|
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
@@ -3847,9 +4728,14 @@ function feedbackChange(from, to) {
|
|
|
3847
4728
|
return { from: from === undefined ? null : from, to: to === undefined ? null : to };
|
|
3848
4729
|
}
|
|
3849
4730
|
|
|
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
|
|
4731
|
+
function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
|
|
4732
|
+
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
4733
|
+
const documentId = firstNonEmptyString(
|
|
4734
|
+
args["document-id"],
|
|
4735
|
+
args.documentId,
|
|
4736
|
+
args.document_id
|
|
4737
|
+
);
|
|
4738
|
+
const artifact = {
|
|
3853
4739
|
schema_version: 1,
|
|
3854
4740
|
kind: "feedback_refinement",
|
|
3855
4741
|
project_id: firstNonEmptyString(manifest?.project?.id, item?.project_id),
|
|
@@ -3857,9 +4743,10 @@ function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, c
|
|
|
3857
4743
|
title_snapshot: firstNonEmptyString(item?.title),
|
|
3858
4744
|
base_snapshot_hash: firstNonEmptyString(item?.snapshot_hash),
|
|
3859
4745
|
base_updated_at: firstNonEmptyString(item?.updated_at),
|
|
3860
|
-
review_action: action,
|
|
3861
|
-
reason: reason || null,
|
|
3862
|
-
|
|
4746
|
+
review_action: action,
|
|
4747
|
+
reason: reason || null,
|
|
4748
|
+
document_id: documentId || undefined,
|
|
4749
|
+
changes,
|
|
3863
4750
|
force: Boolean(args.force) || undefined,
|
|
3864
4751
|
client_session_id: clientSessionId || undefined,
|
|
3865
4752
|
created_at: new Date().toISOString(),
|
|
@@ -3911,11 +4798,13 @@ function summarizeFeedbackRefinementResult(data) {
|
|
|
3911
4798
|
new_snapshot_hash: data?.new_snapshot_hash || data?.feedback?.snapshot_hash || null,
|
|
3912
4799
|
diff_count: Array.isArray(data?.diffs) ? data.diffs.length : 0,
|
|
3913
4800
|
diffs: data?.diffs || [],
|
|
3914
|
-
warnings: data?.warnings || [],
|
|
3915
|
-
errors: data?.errors || [],
|
|
3916
|
-
history_id: data?.history?.history_id || null,
|
|
3917
|
-
|
|
3918
|
-
|
|
4801
|
+
warnings: data?.warnings || [],
|
|
4802
|
+
errors: data?.errors || [],
|
|
4803
|
+
history_id: data?.history?.history_id || null,
|
|
4804
|
+
feedback: data?.feedback || null,
|
|
4805
|
+
history: data?.history || null,
|
|
4806
|
+
};
|
|
4807
|
+
}
|
|
3919
4808
|
|
|
3920
4809
|
function missionOpsThreads(payload) {
|
|
3921
4810
|
if (Array.isArray(payload?.threads)) return payload.threads;
|
|
@@ -4563,7 +5452,7 @@ async function reviewProjectSuggestions({ apiBase, key, timeoutMs, payload, idem
|
|
|
4563
5452
|
});
|
|
4564
5453
|
}
|
|
4565
5454
|
|
|
4566
|
-
async function runCreatePrd(args) {
|
|
5455
|
+
async function runCreatePrd(args) {
|
|
4567
5456
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
4568
5457
|
if (!key) {
|
|
4569
5458
|
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
@@ -4585,13 +5474,24 @@ async function runCreatePrd(args) {
|
|
|
4585
5474
|
.flatMap((value) => toStringArray(value))
|
|
4586
5475
|
.map((item) => item.trim())
|
|
4587
5476
|
.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
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
5477
|
+
const providedPaths = explicitPaths.length ? explicitPaths.concat(positionalPaths) : positionalPaths;
|
|
5478
|
+
const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && providedPaths.length === 0));
|
|
5479
|
+
const documentSetMode = Boolean(
|
|
5480
|
+
args["document-set"] || args.documentSet || args.document_set
|
|
5481
|
+
);
|
|
5482
|
+
|
|
5483
|
+
if (useStdin && providedPaths.length) {
|
|
5484
|
+
console.error("Use either file paths or --stdin for create-prd, not both.");
|
|
5485
|
+
process.exit(1);
|
|
5486
|
+
}
|
|
5487
|
+
if (documentSetMode && useStdin) {
|
|
5488
|
+
console.error("--document-set requires one to three markdown file paths; --stdin is not supported.");
|
|
5489
|
+
process.exit(1);
|
|
5490
|
+
}
|
|
5491
|
+
if (documentSetMode && (providedPaths.length < 1 || providedPaths.length > 3)) {
|
|
5492
|
+
console.error("--document-set requires between one and three markdown file paths.");
|
|
5493
|
+
process.exit(1);
|
|
5494
|
+
}
|
|
4595
5495
|
|
|
4596
5496
|
const buildCreatePrdPayload = ({ sourceText, filePath, includeClientRef = false }) => {
|
|
4597
5497
|
const trimmedSource = String(sourceText || "").trim();
|
|
@@ -4623,8 +5523,8 @@ async function runCreatePrd(args) {
|
|
|
4623
5523
|
return payload;
|
|
4624
5524
|
};
|
|
4625
5525
|
|
|
4626
|
-
let payloads = [];
|
|
4627
|
-
if (useStdin) {
|
|
5526
|
+
let payloads = [];
|
|
5527
|
+
if (useStdin) {
|
|
4628
5528
|
try {
|
|
4629
5529
|
payloads = [
|
|
4630
5530
|
buildCreatePrdPayload({
|
|
@@ -4643,21 +5543,72 @@ async function runCreatePrd(args) {
|
|
|
4643
5543
|
printHelp();
|
|
4644
5544
|
process.exit(1);
|
|
4645
5545
|
}
|
|
4646
|
-
try {
|
|
4647
|
-
|
|
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
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
5546
|
+
try {
|
|
5547
|
+
const resolvedFiles = providedPaths.map((providedPath) => {
|
|
5548
|
+
const absPath = path.resolve(providedPath);
|
|
5549
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
5550
|
+
throw new Error(`PRD file not found: ${absPath}`);
|
|
5551
|
+
}
|
|
5552
|
+
return {
|
|
5553
|
+
absPath,
|
|
5554
|
+
sourceText: fs.readFileSync(absPath, "utf8"),
|
|
5555
|
+
};
|
|
5556
|
+
});
|
|
5557
|
+
if (documentSetMode) {
|
|
5558
|
+
const documents = resolvedFiles.map(({ absPath, sourceText }, index) => {
|
|
5559
|
+
const extension = path.extname(absPath).toLowerCase();
|
|
5560
|
+
if (![".md", ".markdown"].includes(extension)) {
|
|
5561
|
+
throw new Error(`PRD document-set files must use .md or .markdown: ${absPath}`);
|
|
5562
|
+
}
|
|
5563
|
+
if (!String(sourceText || "").trim()) {
|
|
5564
|
+
throw new Error(`PRD content is empty: ${absPath}`);
|
|
5565
|
+
}
|
|
5566
|
+
if (Array.from(sourceText).length > MAX_PRD_DOCUMENT_MARKDOWN_CHARS) {
|
|
5567
|
+
throw new Error(
|
|
5568
|
+
`PRD document-set files must contain at most ${MAX_PRD_DOCUMENT_MARKDOWN_CHARS} characters: ${absPath}`,
|
|
5569
|
+
);
|
|
5570
|
+
}
|
|
5571
|
+
const clientRef = path.relative(process.cwd(), absPath) || path.basename(absPath);
|
|
5572
|
+
return {
|
|
5573
|
+
client_ref: clientRef,
|
|
5574
|
+
filename: path.basename(absPath),
|
|
5575
|
+
title: String(
|
|
5576
|
+
extractMarkdownTitle(sourceText) || path.parse(absPath).name || `Document ${index + 1}`
|
|
5577
|
+
).trim(),
|
|
5578
|
+
source_markdown: sourceText,
|
|
5579
|
+
};
|
|
5580
|
+
});
|
|
5581
|
+
const parentTitle = String(args.title || documents[0]?.title || "").trim();
|
|
5582
|
+
if (!parentTitle) {
|
|
5583
|
+
throw new Error("A title is required for a PRD document set.");
|
|
5584
|
+
}
|
|
5585
|
+
const documentSetPayload = {
|
|
5586
|
+
title: parentTitle,
|
|
5587
|
+
documents,
|
|
5588
|
+
};
|
|
5589
|
+
const description = String(
|
|
5590
|
+
args.description || args["feedback-text"] || args.feedbackText || ""
|
|
5591
|
+
).trim();
|
|
5592
|
+
if (description) documentSetPayload.description = description;
|
|
5593
|
+
const priority = firstNonEmptyString(args.priority);
|
|
5594
|
+
const status = firstNonEmptyString(args.status);
|
|
5595
|
+
const tags = parseFeedbackTags(args);
|
|
5596
|
+
if (priority) documentSetPayload.priority = priority;
|
|
5597
|
+
if (status) documentSetPayload.status = status;
|
|
5598
|
+
if (tags) documentSetPayload.tags = tags;
|
|
5599
|
+
payloads = [documentSetPayload];
|
|
5600
|
+
} else {
|
|
5601
|
+
payloads = resolvedFiles.map(({ absPath, sourceText }) =>
|
|
5602
|
+
buildCreatePrdPayload({
|
|
5603
|
+
sourceText,
|
|
5604
|
+
filePath: absPath,
|
|
5605
|
+
includeClientRef: providedPaths.length > 1,
|
|
5606
|
+
})
|
|
5607
|
+
);
|
|
5608
|
+
}
|
|
5609
|
+
} catch (err) {
|
|
5610
|
+
console.error(err?.message || err);
|
|
5611
|
+
process.exit(1);
|
|
4661
5612
|
}
|
|
4662
5613
|
}
|
|
4663
5614
|
|
|
@@ -4674,7 +5625,10 @@ async function runCreatePrd(args) {
|
|
|
4674
5625
|
if (clientSessionId) payload.client_session_id = clientSessionId;
|
|
4675
5626
|
payload = withWriteApproval(args, payload, {
|
|
4676
5627
|
artifactKind: "markdown",
|
|
4677
|
-
targets:
|
|
5628
|
+
targets: documentSetMode
|
|
5629
|
+
? payload.documents.map((document) => document.client_ref)
|
|
5630
|
+
: [firstNonEmptyString(payload.client_ref, payload.title, "create-prd")],
|
|
5631
|
+
batchCount: documentSetMode ? 1 : null,
|
|
4678
5632
|
});
|
|
4679
5633
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
4680
5634
|
args,
|
|
@@ -4707,9 +5661,12 @@ async function runCreatePrd(args) {
|
|
|
4707
5661
|
feedback_id: data.feedback_id || null,
|
|
4708
5662
|
project_id: data.project_id || null,
|
|
4709
5663
|
title: data.title || null,
|
|
4710
|
-
status: data.status || null,
|
|
4711
|
-
deterministic: data.deterministic === true,
|
|
4712
|
-
|
|
5664
|
+
status: data.status || null,
|
|
5665
|
+
deterministic: data.deterministic === true,
|
|
5666
|
+
document_count: data.document_count || null,
|
|
5667
|
+
primary_document_id: data.primary_document_id || null,
|
|
5668
|
+
documents: Array.isArray(data.documents) ? data.documents : [],
|
|
5669
|
+
},
|
|
4713
5670
|
null,
|
|
4714
5671
|
2
|
|
4715
5672
|
)
|
|
@@ -4787,10 +5744,386 @@ async function runCreatePrd(args) {
|
|
|
4787
5744
|
if (message && status !== "created") line.push(`- ${message}`);
|
|
4788
5745
|
console.log(line.join(" "));
|
|
4789
5746
|
}
|
|
4790
|
-
if (aggregated.failed_count > 0) process.exitCode = 1;
|
|
4791
|
-
}
|
|
4792
|
-
|
|
4793
|
-
|
|
5747
|
+
if (aggregated.failed_count > 0) process.exitCode = 1;
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
function readJsonFileQuiet(filePath) {
|
|
5751
|
+
try {
|
|
5752
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
5753
|
+
} catch {
|
|
5754
|
+
return null;
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
|
|
5758
|
+
function packageRuntimeInfo() {
|
|
5759
|
+
const corePackage = readJsonFileQuiet(path.join(__dirname, "package.json")) || {};
|
|
5760
|
+
let wrapperPackage = null;
|
|
5761
|
+
const localWrapperPath = path.resolve(__dirname, "..", "myte", "package.json");
|
|
5762
|
+
if (fs.existsSync(localWrapperPath)) {
|
|
5763
|
+
wrapperPackage = readJsonFileQuiet(localWrapperPath);
|
|
5764
|
+
}
|
|
5765
|
+
if (!wrapperPackage) {
|
|
5766
|
+
try {
|
|
5767
|
+
wrapperPackage = require("myte/package.json");
|
|
5768
|
+
} catch {
|
|
5769
|
+
wrapperPackage = null;
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
const expectedCoreVersion =
|
|
5773
|
+
wrapperPackage?.dependencies?.["@mytegroupinc/myte-core"] || null;
|
|
5774
|
+
const runtimeDependencies = Object.entries(corePackage.dependencies || {})
|
|
5775
|
+
.map(([name, version]) => ({
|
|
5776
|
+
name,
|
|
5777
|
+
version: String(version),
|
|
5778
|
+
}))
|
|
5779
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
5780
|
+
return {
|
|
5781
|
+
wrapper: {
|
|
5782
|
+
name: wrapperPackage?.name || "myte",
|
|
5783
|
+
version: wrapperPackage?.version || null,
|
|
5784
|
+
},
|
|
5785
|
+
core: {
|
|
5786
|
+
name: corePackage?.name || "@mytegroupinc/myte-core",
|
|
5787
|
+
version: corePackage?.version || null,
|
|
5788
|
+
},
|
|
5789
|
+
compatible:
|
|
5790
|
+
!wrapperPackage ||
|
|
5791
|
+
!expectedCoreVersion ||
|
|
5792
|
+
expectedCoreVersion === corePackage?.version,
|
|
5793
|
+
expected_core_version: expectedCoreVersion,
|
|
5794
|
+
runtime_dependencies: runtimeDependencies,
|
|
5795
|
+
install_graph: {
|
|
5796
|
+
package_count: (wrapperPackage ? 2 : 1) + runtimeDependencies.length,
|
|
5797
|
+
explanation: wrapperPackage
|
|
5798
|
+
? "public wrapper + CLI core + core runtime dependencies"
|
|
5799
|
+
: "CLI core + core runtime dependencies",
|
|
5800
|
+
},
|
|
5801
|
+
};
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
function projectKeySummary(key = getProjectApiKey()) {
|
|
5805
|
+
const normalized = String(key || "").trim();
|
|
5806
|
+
return {
|
|
5807
|
+
present: Boolean(normalized),
|
|
5808
|
+
source: process.env.MYTE_API_KEY
|
|
5809
|
+
? "MYTE_API_KEY"
|
|
5810
|
+
: process.env.MYTE_PROJECT_API_KEY
|
|
5811
|
+
? "MYTE_PROJECT_API_KEY"
|
|
5812
|
+
: null,
|
|
5813
|
+
fingerprint: normalized
|
|
5814
|
+
? createHash("sha256").update(normalized).digest("hex").slice(0, 12)
|
|
5815
|
+
: null,
|
|
5816
|
+
};
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
function probeDns(
|
|
5820
|
+
hostname,
|
|
5821
|
+
timeoutMs,
|
|
5822
|
+
{
|
|
5823
|
+
lookup = dns.promises.lookup.bind(dns.promises),
|
|
5824
|
+
setTimeoutFn = setTimeout,
|
|
5825
|
+
clearTimeoutFn = clearTimeout,
|
|
5826
|
+
} = {},
|
|
5827
|
+
) {
|
|
5828
|
+
if (net.isIP(hostname)) {
|
|
5829
|
+
return Promise.resolve({
|
|
5830
|
+
ok: true,
|
|
5831
|
+
hostname,
|
|
5832
|
+
addresses: [{ address: hostname, family: net.isIP(hostname) }],
|
|
5833
|
+
});
|
|
5834
|
+
}
|
|
5835
|
+
return new Promise((resolve) => {
|
|
5836
|
+
let settled = false;
|
|
5837
|
+
let timer;
|
|
5838
|
+
const finish = (payload) => {
|
|
5839
|
+
if (settled) return;
|
|
5840
|
+
settled = true;
|
|
5841
|
+
if (timer !== undefined) clearTimeoutFn(timer);
|
|
5842
|
+
resolve(payload);
|
|
5843
|
+
};
|
|
5844
|
+
timer = setTimeoutFn(() => {
|
|
5845
|
+
const error = new Error("DNS probe timed out.");
|
|
5846
|
+
error.code = "ETIMEDOUT";
|
|
5847
|
+
finish({
|
|
5848
|
+
ok: false,
|
|
5849
|
+
hostname,
|
|
5850
|
+
failure: publicRequestFailure(
|
|
5851
|
+
createNetworkRequestError(error, `https://${hostname}`, { timedOut: true }),
|
|
5852
|
+
),
|
|
5853
|
+
});
|
|
5854
|
+
}, timeoutMs);
|
|
5855
|
+
Promise.resolve()
|
|
5856
|
+
.then(() => lookup(hostname, { all: true }))
|
|
5857
|
+
.then(
|
|
5858
|
+
(addresses) => {
|
|
5859
|
+
finish({
|
|
5860
|
+
ok: true,
|
|
5861
|
+
hostname,
|
|
5862
|
+
addresses: addresses.map((entry) => ({
|
|
5863
|
+
address: entry.address,
|
|
5864
|
+
family: entry.family,
|
|
5865
|
+
})),
|
|
5866
|
+
});
|
|
5867
|
+
},
|
|
5868
|
+
(error) => {
|
|
5869
|
+
finish({
|
|
5870
|
+
ok: false,
|
|
5871
|
+
hostname,
|
|
5872
|
+
failure: publicRequestFailure(
|
|
5873
|
+
createNetworkRequestError(error, `https://${hostname}`),
|
|
5874
|
+
),
|
|
5875
|
+
});
|
|
5876
|
+
},
|
|
5877
|
+
);
|
|
5878
|
+
});
|
|
5879
|
+
}
|
|
5880
|
+
|
|
5881
|
+
function probeDirectTransport(apiBase, timeoutMs) {
|
|
5882
|
+
const parsed = new URL(apiBase);
|
|
5883
|
+
const secure = parsed.protocol === "https:";
|
|
5884
|
+
const port = Number(parsed.port || (secure ? 443 : 80));
|
|
5885
|
+
const startedAt = Date.now();
|
|
5886
|
+
return new Promise((resolve) => {
|
|
5887
|
+
let settled = false;
|
|
5888
|
+
const finish = (payload) => {
|
|
5889
|
+
if (settled) return;
|
|
5890
|
+
settled = true;
|
|
5891
|
+
clearTimeout(timer);
|
|
5892
|
+
try {
|
|
5893
|
+
socket.destroy();
|
|
5894
|
+
} catch {}
|
|
5895
|
+
resolve({
|
|
5896
|
+
...payload,
|
|
5897
|
+
protocol: secure ? "tls" : "tcp",
|
|
5898
|
+
host: parsed.hostname,
|
|
5899
|
+
port,
|
|
5900
|
+
duration_ms: Date.now() - startedAt,
|
|
5901
|
+
});
|
|
5902
|
+
};
|
|
5903
|
+
const options = {
|
|
5904
|
+
host: parsed.hostname,
|
|
5905
|
+
port,
|
|
5906
|
+
...(secure && !net.isIP(parsed.hostname) ? { servername: parsed.hostname } : {}),
|
|
5907
|
+
};
|
|
5908
|
+
const socket = secure ? tls.connect(options) : net.connect(options);
|
|
5909
|
+
const timer = setTimeout(() => {
|
|
5910
|
+
const error = new Error("Transport probe timed out.");
|
|
5911
|
+
error.code = "ETIMEDOUT";
|
|
5912
|
+
finish({
|
|
5913
|
+
ok: false,
|
|
5914
|
+
failure: publicRequestFailure(
|
|
5915
|
+
createNetworkRequestError(error, apiBase, { timedOut: true }),
|
|
5916
|
+
),
|
|
5917
|
+
});
|
|
5918
|
+
}, timeoutMs);
|
|
5919
|
+
socket.once(secure ? "secureConnect" : "connect", () => {
|
|
5920
|
+
finish({
|
|
5921
|
+
ok: true,
|
|
5922
|
+
authorized: secure ? socket.authorized === true : null,
|
|
5923
|
+
tls_protocol: secure ? socket.getProtocol?.() || null : null,
|
|
5924
|
+
});
|
|
5925
|
+
});
|
|
5926
|
+
socket.once("error", (error) => {
|
|
5927
|
+
finish({
|
|
5928
|
+
ok: false,
|
|
5929
|
+
failure: publicRequestFailure(createNetworkRequestError(error, apiBase)),
|
|
5930
|
+
});
|
|
5931
|
+
});
|
|
5932
|
+
});
|
|
5933
|
+
}
|
|
5934
|
+
|
|
5935
|
+
async function runInfo(args, envPath) {
|
|
5936
|
+
const packages = packageRuntimeInfo();
|
|
5937
|
+
const payload = {
|
|
5938
|
+
command: "myte",
|
|
5939
|
+
packages,
|
|
5940
|
+
node_version: process.version,
|
|
5941
|
+
platform: `${process.platform}-${process.arch}`,
|
|
5942
|
+
api_base: resolveApiBase(args),
|
|
5943
|
+
project_key: projectKeySummary(),
|
|
5944
|
+
proxy: proxyEnvironmentSummary(),
|
|
5945
|
+
workspace: {
|
|
5946
|
+
cwd: process.cwd(),
|
|
5947
|
+
env_file: envPath,
|
|
5948
|
+
},
|
|
5949
|
+
};
|
|
5950
|
+
if (args.json) {
|
|
5951
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
5952
|
+
return;
|
|
5953
|
+
}
|
|
5954
|
+
console.log(`myte ${packages.wrapper.version || packages.core.version || "unknown"}`);
|
|
5955
|
+
console.log(`core: ${packages.core.version || "unknown"}`);
|
|
5956
|
+
console.log(`wrapper/core compatible: ${packages.compatible ? "yes" : "no"}`);
|
|
5957
|
+
console.log(
|
|
5958
|
+
`runtime dependencies: ${
|
|
5959
|
+
packages.runtime_dependencies.length
|
|
5960
|
+
? packages.runtime_dependencies
|
|
5961
|
+
.map((dependency) => `${dependency.name}@${dependency.version}`)
|
|
5962
|
+
.join(", ")
|
|
5963
|
+
: "none"
|
|
5964
|
+
}`,
|
|
5965
|
+
);
|
|
5966
|
+
console.log(`npm install graph: ${packages.install_graph.package_count} packages`);
|
|
5967
|
+
console.log(`node: ${payload.node_version}`);
|
|
5968
|
+
console.log(`api: ${payload.api_base}`);
|
|
5969
|
+
console.log(`project key: ${payload.project_key.present ? "present" : "missing"}`);
|
|
5970
|
+
console.log(`proxy: ${payload.proxy.configured ? "configured" : "direct"}`);
|
|
5971
|
+
console.log(`workspace: ${payload.workspace.cwd}`);
|
|
5972
|
+
}
|
|
5973
|
+
|
|
5974
|
+
async function runVersion(args) {
|
|
5975
|
+
const packages = packageRuntimeInfo();
|
|
5976
|
+
const version = packages.wrapper.version || packages.core.version || "unknown";
|
|
5977
|
+
if (args.verbose || args.json) {
|
|
5978
|
+
const payload = {
|
|
5979
|
+
version,
|
|
5980
|
+
...packages,
|
|
5981
|
+
node_version: process.version,
|
|
5982
|
+
compatible: packages.compatible,
|
|
5983
|
+
};
|
|
5984
|
+
if (args.json) {
|
|
5985
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
5986
|
+
} else {
|
|
5987
|
+
console.log(`myte ${version}`);
|
|
5988
|
+
console.log(`core ${packages.core.version || "unknown"}`);
|
|
5989
|
+
console.log(
|
|
5990
|
+
`runtime dependencies: ${
|
|
5991
|
+
packages.runtime_dependencies.length
|
|
5992
|
+
? packages.runtime_dependencies
|
|
5993
|
+
.map((dependency) => `${dependency.name}@${dependency.version}`)
|
|
5994
|
+
.join(", ")
|
|
5995
|
+
: "none"
|
|
5996
|
+
}`,
|
|
5997
|
+
);
|
|
5998
|
+
console.log(`npm install graph: ${packages.install_graph.package_count} packages`);
|
|
5999
|
+
console.log(`node ${process.version}`);
|
|
6000
|
+
console.log(`wrapper/core compatible: ${packages.compatible ? "yes" : "no"}`);
|
|
6001
|
+
}
|
|
6002
|
+
return;
|
|
6003
|
+
}
|
|
6004
|
+
console.log(version);
|
|
6005
|
+
}
|
|
6006
|
+
|
|
6007
|
+
async function runDoctor(args, envPath) {
|
|
6008
|
+
const timeoutMs = Math.max(
|
|
6009
|
+
1_000,
|
|
6010
|
+
Math.min(30_000, Number(args["timeout-ms"] || args.timeoutMs || 10_000) || 10_000),
|
|
6011
|
+
);
|
|
6012
|
+
const apiBase = resolveApiBase(args);
|
|
6013
|
+
const parsedBase = new URL(apiBase);
|
|
6014
|
+
const key = getProjectApiKey();
|
|
6015
|
+
const proxy = proxyEnvironmentSummary();
|
|
6016
|
+
const packages = packageRuntimeInfo();
|
|
6017
|
+
const payload = {
|
|
6018
|
+
schema_version: 1,
|
|
6019
|
+
command: "myte doctor",
|
|
6020
|
+
generated_at: new Date().toISOString(),
|
|
6021
|
+
packages,
|
|
6022
|
+
runtime: {
|
|
6023
|
+
node_version: process.version,
|
|
6024
|
+
platform: `${process.platform}-${process.arch}`,
|
|
6025
|
+
transport: null,
|
|
6026
|
+
},
|
|
6027
|
+
api: {
|
|
6028
|
+
base: apiBase,
|
|
6029
|
+
endpoint: parsedBase.host,
|
|
6030
|
+
},
|
|
6031
|
+
project_key: projectKeySummary(key),
|
|
6032
|
+
proxy,
|
|
6033
|
+
dns: null,
|
|
6034
|
+
direct_transport: null,
|
|
6035
|
+
config_probe: null,
|
|
6036
|
+
workspace: {
|
|
6037
|
+
cwd: process.cwd(),
|
|
6038
|
+
env_file: envPath,
|
|
6039
|
+
mode: "unknown",
|
|
6040
|
+
root: process.cwd(),
|
|
6041
|
+
configured_repos: [],
|
|
6042
|
+
found_repos: [],
|
|
6043
|
+
missing_repos: [],
|
|
6044
|
+
},
|
|
6045
|
+
ok: false,
|
|
6046
|
+
failure_layer: null,
|
|
6047
|
+
};
|
|
6048
|
+
|
|
6049
|
+
try {
|
|
6050
|
+
payload.runtime.transport = (await getFetchRuntime()).transport;
|
|
6051
|
+
} catch (error) {
|
|
6052
|
+
payload.runtime.transport = "unavailable";
|
|
6053
|
+
payload.config_probe = {
|
|
6054
|
+
ok: false,
|
|
6055
|
+
failure: publicRequestFailure(error),
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
payload.dns = await probeDns(parsedBase.hostname, timeoutMs);
|
|
6059
|
+
payload.direct_transport = await probeDirectTransport(apiBase, timeoutMs);
|
|
6060
|
+
|
|
6061
|
+
if (!payload.config_probe && key) {
|
|
6062
|
+
try {
|
|
6063
|
+
const config = await fetchProjectConfig({ apiBase, key, timeoutMs });
|
|
6064
|
+
const repoNames = Array.isArray(config.repo_names) ? config.repo_names : [];
|
|
6065
|
+
const repoBindings = Array.isArray(config.repo_bindings) ? config.repo_bindings : [];
|
|
6066
|
+
const resolved = repoBindings.length
|
|
6067
|
+
? resolveConfiguredRepoBindings(repoBindings)
|
|
6068
|
+
: resolvePortableWorkspace(repoNames);
|
|
6069
|
+
payload.config_probe = {
|
|
6070
|
+
ok: true,
|
|
6071
|
+
project_id: config.project_id || null,
|
|
6072
|
+
repo_count: repoNames.length,
|
|
6073
|
+
};
|
|
6074
|
+
payload.workspace = {
|
|
6075
|
+
...payload.workspace,
|
|
6076
|
+
mode: resolved.mode || "unknown",
|
|
6077
|
+
root: resolved.root || process.cwd(),
|
|
6078
|
+
configured_repos: repoNames,
|
|
6079
|
+
found_repos: (resolved.repos || []).map((repo) => repo.name),
|
|
6080
|
+
missing_repos: resolved.missing || [],
|
|
6081
|
+
};
|
|
6082
|
+
} catch (error) {
|
|
6083
|
+
payload.config_probe = {
|
|
6084
|
+
ok: false,
|
|
6085
|
+
failure: publicRequestFailure(error),
|
|
6086
|
+
};
|
|
6087
|
+
}
|
|
6088
|
+
} else if (!payload.config_probe) {
|
|
6089
|
+
payload.config_probe = {
|
|
6090
|
+
ok: false,
|
|
6091
|
+
skipped: true,
|
|
6092
|
+
reason: "project_key_missing",
|
|
6093
|
+
};
|
|
6094
|
+
}
|
|
6095
|
+
|
|
6096
|
+
payload.ok = payload.config_probe.ok === true;
|
|
6097
|
+
const firstFailure =
|
|
6098
|
+
payload.config_probe.failure ||
|
|
6099
|
+
(!payload.direct_transport.ok ? payload.direct_transport.failure : null) ||
|
|
6100
|
+
(!payload.dns.ok ? payload.dns.failure : null);
|
|
6101
|
+
payload.failure_layer = firstFailure?.layer || null;
|
|
6102
|
+
|
|
6103
|
+
if (args.json) {
|
|
6104
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
6105
|
+
} else {
|
|
6106
|
+
console.log(`Myte doctor: ${payload.ok ? "reachable" : "not ready"}`);
|
|
6107
|
+
console.log(`API: ${payload.api.base}`);
|
|
6108
|
+
console.log(`Key: ${payload.project_key.present ? "present" : "missing"}`);
|
|
6109
|
+
console.log(`Proxy: ${payload.proxy.configured ? "configured" : "direct"}`);
|
|
6110
|
+
console.log(`DNS: ${payload.dns.ok ? "ok" : "failed"}`);
|
|
6111
|
+
console.log(
|
|
6112
|
+
`Direct ${payload.direct_transport.protocol || "transport"}: ${
|
|
6113
|
+
payload.direct_transport.ok ? "ok" : "failed"
|
|
6114
|
+
}`,
|
|
6115
|
+
);
|
|
6116
|
+
console.log(`Authenticated config: ${payload.config_probe.ok ? "ok" : "failed"}`);
|
|
6117
|
+
if (firstFailure) {
|
|
6118
|
+
console.log(`Failure layer: ${firstFailure.layer || "unknown"}`);
|
|
6119
|
+
console.log(`Cause: ${firstFailure.code || "unknown"}`);
|
|
6120
|
+
console.log(`Detail: ${firstFailure.detail || "Request failed."}`);
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
if (!payload.ok) process.exitCode = 1;
|
|
6124
|
+
}
|
|
6125
|
+
|
|
6126
|
+
async function runConfig(args) {
|
|
4794
6127
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
4795
6128
|
if (!key) {
|
|
4796
6129
|
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
@@ -5074,7 +6407,7 @@ async function runSyncQaqc(args) {
|
|
|
5074
6407
|
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
5075
6408
|
}
|
|
5076
6409
|
|
|
5077
|
-
async function runFeedbackSync(args) {
|
|
6410
|
+
async function runFeedbackSync(args) {
|
|
5078
6411
|
const key = getProjectApiKey();
|
|
5079
6412
|
if (!key) {
|
|
5080
6413
|
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
@@ -5083,17 +6416,59 @@ async function runFeedbackSync(args) {
|
|
|
5083
6416
|
|
|
5084
6417
|
const timeoutMs = resolveTimeoutMs(args);
|
|
5085
6418
|
const apiBase = resolveApiBase(args);
|
|
5086
|
-
const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
6419
|
+
const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
|
|
6420
|
+
let pageSize;
|
|
6421
|
+
let maxItems;
|
|
6422
|
+
let commentAttachmentTextLimit;
|
|
6423
|
+
try {
|
|
6424
|
+
pageSize = resolveBoundedInteger(
|
|
6425
|
+
args["page-size"] || args.pageSize || args.page_size,
|
|
6426
|
+
{ fallback: DEFAULT_FEEDBACK_SYNC_PAGE_SIZE, min: 1, max: 500, label: "page-size" }
|
|
6427
|
+
);
|
|
6428
|
+
maxItems = resolveBoundedInteger(
|
|
6429
|
+
args.limit,
|
|
6430
|
+
{ fallback: 0, min: 0, max: 100_000, label: "limit" }
|
|
6431
|
+
);
|
|
6432
|
+
commentAttachmentTextLimit = resolveBoundedInteger(
|
|
6433
|
+
args["comment-attachment-limit"]
|
|
6434
|
+
|| args.commentAttachmentLimit
|
|
6435
|
+
|| args.comment_attachment_limit,
|
|
6436
|
+
{
|
|
6437
|
+
fallback: 20,
|
|
6438
|
+
min: 0,
|
|
6439
|
+
max: 100,
|
|
6440
|
+
label: "comment-attachment-limit",
|
|
6441
|
+
}
|
|
6442
|
+
);
|
|
6443
|
+
} catch (err) {
|
|
6444
|
+
console.error(err?.message || err);
|
|
6445
|
+
process.exit(1);
|
|
6446
|
+
}
|
|
6447
|
+
const filters = {
|
|
6448
|
+
status: firstNonEmptyString(args.status) || "",
|
|
6449
|
+
source: firstNonEmptyString(args.source) || "",
|
|
6450
|
+
includePrdText,
|
|
6451
|
+
includeCommentTurns: true,
|
|
6452
|
+
includeCommentAttachmentText: includePrdText,
|
|
6453
|
+
commentAttachmentTextLimit,
|
|
6454
|
+
};
|
|
5093
6455
|
|
|
5094
|
-
let snapshot;
|
|
5095
|
-
try {
|
|
5096
|
-
snapshot = await
|
|
6456
|
+
let snapshot;
|
|
6457
|
+
try {
|
|
6458
|
+
snapshot = await fetchCompleteFeedbackSyncSnapshot({
|
|
6459
|
+
apiBase,
|
|
6460
|
+
key,
|
|
6461
|
+
timeoutMs,
|
|
6462
|
+
filters,
|
|
6463
|
+
pageSize,
|
|
6464
|
+
maxItems,
|
|
6465
|
+
onPage: args.json
|
|
6466
|
+
? undefined
|
|
6467
|
+
: ({ page, received, synced, total }) => {
|
|
6468
|
+
const totalText = Number.isFinite(Number(total)) ? `/${total}` : "";
|
|
6469
|
+
console.error(`Feedback sync page ${page}: +${received} (${synced}${totalText})`);
|
|
6470
|
+
},
|
|
6471
|
+
});
|
|
5097
6472
|
} catch (err) {
|
|
5098
6473
|
console.error("Failed to fetch feedback sync snapshot:", err?.message || err);
|
|
5099
6474
|
process.exit(1);
|
|
@@ -5130,8 +6505,10 @@ async function runFeedbackSync(args) {
|
|
|
5130
6505
|
filters: snapshot.filters && typeof snapshot.filters === "object" ? snapshot.filters : filters,
|
|
5131
6506
|
counts,
|
|
5132
6507
|
snapshot_hash: snapshot.snapshot_hash || null,
|
|
5133
|
-
generated_at: snapshot.generated_at || null,
|
|
5134
|
-
|
|
6508
|
+
generated_at: snapshot.generated_at || null,
|
|
6509
|
+
pagination: snapshot.pagination || null,
|
|
6510
|
+
diagnostics: snapshot.diagnostics || null,
|
|
6511
|
+
dry_run: dryRun,
|
|
5135
6512
|
};
|
|
5136
6513
|
|
|
5137
6514
|
if (dryRun) {
|
|
@@ -5172,22 +6549,25 @@ async function runFeedbackSync(args) {
|
|
|
5172
6549
|
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
5173
6550
|
}
|
|
5174
6551
|
|
|
5175
|
-
async function refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeoutMs }) {
|
|
6552
|
+
async function refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeoutMs }) {
|
|
5176
6553
|
if (args.sync === false || args["no-sync"] || args.noSync || args.no_sync) {
|
|
5177
6554
|
return { skipped: true };
|
|
5178
6555
|
}
|
|
5179
6556
|
try {
|
|
5180
|
-
const snapshot = await
|
|
5181
|
-
apiBase,
|
|
5182
|
-
key,
|
|
5183
|
-
timeoutMs,
|
|
5184
|
-
filters: {
|
|
6557
|
+
const snapshot = await fetchCompleteFeedbackSyncSnapshot({
|
|
6558
|
+
apiBase,
|
|
6559
|
+
key,
|
|
6560
|
+
timeoutMs,
|
|
6561
|
+
filters: {
|
|
5185
6562
|
status: "",
|
|
5186
6563
|
source: "",
|
|
5187
|
-
includePrdText: true,
|
|
5188
|
-
includeCommentTurns: true,
|
|
5189
|
-
|
|
5190
|
-
|
|
6564
|
+
includePrdText: true,
|
|
6565
|
+
includeCommentTurns: true,
|
|
6566
|
+
includeCommentAttachmentText: true,
|
|
6567
|
+
commentAttachmentTextLimit: 20,
|
|
6568
|
+
},
|
|
6569
|
+
pageSize: DEFAULT_FEEDBACK_SYNC_PAGE_SIZE,
|
|
6570
|
+
});
|
|
5191
6571
|
const resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
5192
6572
|
const writeResult = writeFeedbackSnapshot({
|
|
5193
6573
|
snapshot,
|
|
@@ -5270,9 +6650,62 @@ async function buildFeedbackDraftForCommand(args, subcommand) {
|
|
|
5270
6650
|
const tags = parseFeedbackTags(args);
|
|
5271
6651
|
if (tags) changes.tags = feedbackChange(Array.isArray(item.tags) ? item.tags : [], tags);
|
|
5272
6652
|
|
|
5273
|
-
const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
|
|
5274
|
-
if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
|
|
5275
|
-
|
|
6653
|
+
const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
|
|
6654
|
+
if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
|
|
6655
|
+
|
|
6656
|
+
const prdFile = firstNonEmptyString(args["prd-file"], args.prdFile, args.prd_file);
|
|
6657
|
+
if (prdFile) {
|
|
6658
|
+
const proposedPath = resolveInputFile(prdFile, "PRD document");
|
|
6659
|
+
const proposedMarkdown = fs.readFileSync(proposedPath, "utf8");
|
|
6660
|
+
if (!proposedMarkdown.trim()) {
|
|
6661
|
+
console.error("PRD document markdown cannot be empty.");
|
|
6662
|
+
process.exit(1);
|
|
6663
|
+
}
|
|
6664
|
+
if (proposedMarkdown.length > 250000) {
|
|
6665
|
+
console.error("PRD document markdown must be at most 250000 characters.");
|
|
6666
|
+
process.exit(1);
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
const documents = Array.isArray(item.prd_documents) ? item.prd_documents : [];
|
|
6670
|
+
const explicitDocumentId = firstNonEmptyString(
|
|
6671
|
+
args["document-id"],
|
|
6672
|
+
args.documentId,
|
|
6673
|
+
args.document_id
|
|
6674
|
+
);
|
|
6675
|
+
const targetDocumentId = explicitDocumentId
|
|
6676
|
+
|| firstNonEmptyString(item.primary_prd_document_id)
|
|
6677
|
+
|| firstNonEmptyString(documents[0]?.document_id);
|
|
6678
|
+
const targetDocument = targetDocumentId
|
|
6679
|
+
? documents.find((document) => String(document?.document_id || "") === targetDocumentId)
|
|
6680
|
+
: null;
|
|
6681
|
+
if (explicitDocumentId && !targetDocument) {
|
|
6682
|
+
console.error(`PRD document not found in feedback.yml: ${explicitDocumentId}. Run \`myte feedback-sync\` first.`);
|
|
6683
|
+
process.exit(1);
|
|
6684
|
+
}
|
|
6685
|
+
if (documents.length > 0 && !targetDocument) {
|
|
6686
|
+
console.error("Unable to resolve the primary PRD document. Run `myte feedback-sync` first.");
|
|
6687
|
+
process.exit(1);
|
|
6688
|
+
}
|
|
6689
|
+
|
|
6690
|
+
let currentMarkdown = "";
|
|
6691
|
+
const localFile = firstNonEmptyString(targetDocument?.local_file, item.prd_file);
|
|
6692
|
+
if (localFile) {
|
|
6693
|
+
const currentPath = path.resolve(paths.targetRoot, localFile);
|
|
6694
|
+
const relativePath = path.relative(paths.targetRoot, currentPath);
|
|
6695
|
+
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath) && fs.existsSync(currentPath)) {
|
|
6696
|
+
currentMarkdown = fs.readFileSync(currentPath, "utf8");
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
changes.prd_markdown = feedbackChange(currentMarkdown, proposedMarkdown);
|
|
6700
|
+
changes.prd_format = feedbackChange(
|
|
6701
|
+
firstNonEmptyString(item.prd_format, "markdown"),
|
|
6702
|
+
"markdown"
|
|
6703
|
+
);
|
|
6704
|
+
if (targetDocumentId) {
|
|
6705
|
+
args["document-id"] = targetDocumentId;
|
|
6706
|
+
}
|
|
6707
|
+
}
|
|
6708
|
+
} else {
|
|
5276
6709
|
console.error("Unknown feedback draft command. Use status, edit, assign, archive, or refine.");
|
|
5277
6710
|
process.exit(1);
|
|
5278
6711
|
}
|
|
@@ -5627,7 +7060,7 @@ async function runFeedbackReviews(args) {
|
|
|
5627
7060
|
}
|
|
5628
7061
|
}
|
|
5629
7062
|
|
|
5630
|
-
function buildFeedbackReviewDecisionPayload(args, action) {
|
|
7063
|
+
function buildFeedbackReviewDecisionPayload(args, action) {
|
|
5631
7064
|
const filePath = firstNonEmptyString(args.file);
|
|
5632
7065
|
let payload = {};
|
|
5633
7066
|
if (filePath) {
|
|
@@ -5639,9 +7072,25 @@ function buildFeedbackReviewDecisionPayload(args, action) {
|
|
|
5639
7072
|
}
|
|
5640
7073
|
const nextPayload = { ...payload };
|
|
5641
7074
|
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
|
-
}
|
|
7075
|
+
if (reason && !nextPayload.reason && !nextPayload.review_reason) {
|
|
7076
|
+
nextPayload.reason = reason;
|
|
7077
|
+
}
|
|
7078
|
+
const verificationEvidence = firstNonEmptyString(
|
|
7079
|
+
args["verification-evidence"],
|
|
7080
|
+
args.verificationEvidence,
|
|
7081
|
+
args.verification_evidence,
|
|
7082
|
+
);
|
|
7083
|
+
if (verificationEvidence && !nextPayload.verification_evidence) {
|
|
7084
|
+
nextPayload.verification_evidence = verificationEvidence;
|
|
7085
|
+
}
|
|
7086
|
+
const deploymentEvidence = firstNonEmptyString(
|
|
7087
|
+
args["deployment-evidence"],
|
|
7088
|
+
args.deploymentEvidence,
|
|
7089
|
+
args.deployment_evidence,
|
|
7090
|
+
);
|
|
7091
|
+
if (deploymentEvidence && !nextPayload.deployment_evidence) {
|
|
7092
|
+
nextPayload.deployment_evidence = deploymentEvidence;
|
|
7093
|
+
}
|
|
5645
7094
|
const finalFile = firstNonEmptyString(args["final-file"], args.finalFile, args.final_file);
|
|
5646
7095
|
if (finalFile) {
|
|
5647
7096
|
nextPayload.final_change_set = readStructuredPayloadFile(finalFile, "Feedback final change set");
|
|
@@ -5681,12 +7130,24 @@ async function runFeedbackReviewDecision(args) {
|
|
|
5681
7130
|
console.error("Missing --request-ids for batch feedback review.");
|
|
5682
7131
|
process.exit(1);
|
|
5683
7132
|
}
|
|
5684
|
-
const reason = firstNonEmptyString(args.reason, args["review-note"], args.reviewNote, args.review_note);
|
|
7133
|
+
const reason = firstNonEmptyString(args.reason, args["review-note"], args.reviewNote, args.review_note);
|
|
7134
|
+
const verificationEvidence = firstNonEmptyString(
|
|
7135
|
+
args["verification-evidence"],
|
|
7136
|
+
args.verificationEvidence,
|
|
7137
|
+
args.verification_evidence,
|
|
7138
|
+
);
|
|
7139
|
+
const deploymentEvidence = firstNonEmptyString(
|
|
7140
|
+
args["deployment-evidence"],
|
|
7141
|
+
args.deploymentEvidence,
|
|
7142
|
+
args.deployment_evidence,
|
|
7143
|
+
);
|
|
5685
7144
|
let payload = {
|
|
5686
7145
|
...(isPlainObject(filePayload) && !Array.isArray(filePayload) ? filePayload : {}),
|
|
5687
7146
|
items,
|
|
5688
7147
|
action,
|
|
5689
7148
|
reason: reason || undefined,
|
|
7149
|
+
verification_evidence: verificationEvidence || undefined,
|
|
7150
|
+
deployment_evidence: deploymentEvidence || undefined,
|
|
5690
7151
|
};
|
|
5691
7152
|
const timeoutMs = resolveTimeoutMs(args);
|
|
5692
7153
|
const apiBase = resolveApiBase(args);
|
|
@@ -6079,12 +7540,21 @@ async function runFeedbackPrdDiff(args) {
|
|
|
6079
7540
|
console.error("Missing --version-id.");
|
|
6080
7541
|
process.exit(1);
|
|
6081
7542
|
}
|
|
6082
|
-
const timeoutMs = resolveTimeoutMs(args);
|
|
6083
|
-
const apiBase = resolveApiBase(args);
|
|
6084
|
-
const compareTo = firstNonEmptyString(args["compare-to"], args.compareTo, args.compare_to);
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
7543
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
7544
|
+
const apiBase = resolveApiBase(args);
|
|
7545
|
+
const compareTo = firstNonEmptyString(args["compare-to"], args.compareTo, args.compare_to);
|
|
7546
|
+
const documentId = firstNonEmptyString(args["document-id"], args.documentId, args.document_id);
|
|
7547
|
+
let data;
|
|
7548
|
+
try {
|
|
7549
|
+
data = await fetchFeedbackPrdVersionDiff({
|
|
7550
|
+
apiBase,
|
|
7551
|
+
key,
|
|
7552
|
+
timeoutMs,
|
|
7553
|
+
feedbackId,
|
|
7554
|
+
versionId,
|
|
7555
|
+
compareTo,
|
|
7556
|
+
documentId,
|
|
7557
|
+
});
|
|
6088
7558
|
} catch (err) {
|
|
6089
7559
|
console.error("Feedback PRD diff failed:", err?.message || err);
|
|
6090
7560
|
process.exit(1);
|
|
@@ -6798,8 +8268,9 @@ async function runQuery(args) {
|
|
|
6798
8268
|
process.exit(1);
|
|
6799
8269
|
}
|
|
6800
8270
|
|
|
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);
|
|
8271
|
+
const includeDiff = Boolean(args["with-diff"] || args.withDiff || args.diff || args.d);
|
|
8272
|
+
const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
|
|
8273
|
+
const verbose = Boolean(args.verbose || args.v);
|
|
6803
8274
|
|
|
6804
8275
|
const fetchRemote = args.fetch !== undefined ? Boolean(args.fetch) : true;
|
|
6805
8276
|
|
|
@@ -6871,11 +8342,32 @@ async function runQuery(args) {
|
|
|
6871
8342
|
process.exit(0);
|
|
6872
8343
|
}
|
|
6873
8344
|
|
|
6874
|
-
let data;
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
if (
|
|
6878
|
-
|
|
8345
|
+
let data;
|
|
8346
|
+
const requestId = queryRequestId(args);
|
|
8347
|
+
try {
|
|
8348
|
+
if (verbose) {
|
|
8349
|
+
const runtime = await getFetchRuntime();
|
|
8350
|
+
console.error(
|
|
8351
|
+
`[myte] query request_id=${requestId} endpoint=${safeEndpointHost(apiBase)} transport=${runtime.transport}`,
|
|
8352
|
+
);
|
|
8353
|
+
}
|
|
8354
|
+
const queued = await withTransientQueryRetries(
|
|
8355
|
+
() =>
|
|
8356
|
+
createAssistantQueryJob({
|
|
8357
|
+
apiBase,
|
|
8358
|
+
key,
|
|
8359
|
+
payload,
|
|
8360
|
+
timeoutMs,
|
|
8361
|
+
requestId,
|
|
8362
|
+
}),
|
|
8363
|
+
{
|
|
8364
|
+
maxAttempts: 3,
|
|
8365
|
+
verbose,
|
|
8366
|
+
label: "query creation",
|
|
8367
|
+
},
|
|
8368
|
+
);
|
|
8369
|
+
if (queued.answer) {
|
|
8370
|
+
data = queued;
|
|
6879
8371
|
} else {
|
|
6880
8372
|
const jobId = firstNonEmptyString(queued.job_id, queued.id);
|
|
6881
8373
|
if (!jobId) {
|
|
@@ -6886,21 +8378,24 @@ async function runQuery(args) {
|
|
|
6886
8378
|
let finalStatus = null;
|
|
6887
8379
|
let pollDelayMs = 2_000;
|
|
6888
8380
|
do {
|
|
6889
|
-
await sleep(pollDelayMs);
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
8381
|
+
await sleep(pollDelayMs);
|
|
8382
|
+
finalStatus = await withTransientQueryRetries(
|
|
8383
|
+
() =>
|
|
8384
|
+
fetchAssistantQueryJobStatus({
|
|
8385
|
+
apiBase,
|
|
8386
|
+
key,
|
|
8387
|
+
timeoutMs,
|
|
8388
|
+
jobId,
|
|
8389
|
+
requestId,
|
|
8390
|
+
}),
|
|
8391
|
+
{
|
|
8392
|
+
maxAttempts: 4,
|
|
8393
|
+
verbose,
|
|
8394
|
+
label: "query status",
|
|
8395
|
+
initialDelayMs: Math.min(2_000, pollDelayMs),
|
|
8396
|
+
},
|
|
8397
|
+
);
|
|
8398
|
+
pollDelayMs = Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
|
|
6904
8399
|
if (["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
|
|
6905
8400
|
break;
|
|
6906
8401
|
}
|
|
@@ -6913,22 +8408,37 @@ async function runQuery(args) {
|
|
|
6913
8408
|
const detail = firstNonEmptyString(finalStatus?.error?.message, finalStatus?.error?.code);
|
|
6914
8409
|
throw new Error(detail || `Query job ${jobId} failed`);
|
|
6915
8410
|
}
|
|
6916
|
-
data = {
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
8411
|
+
data = {
|
|
8412
|
+
job_id: jobId,
|
|
8413
|
+
answer: finalStatus.answer,
|
|
8414
|
+
context_blocks: finalStatus.context_blocks,
|
|
8415
|
+
telemetry: finalStatus.telemetry,
|
|
6920
8416
|
};
|
|
6921
8417
|
}
|
|
6922
|
-
} catch (err) {
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
8418
|
+
} catch (err) {
|
|
8419
|
+
console.error("Assistant query failed:", err?.message || err);
|
|
8420
|
+
process.exit(1);
|
|
8421
|
+
}
|
|
8422
|
+
|
|
8423
|
+
if (args.json) {
|
|
8424
|
+
console.log(
|
|
8425
|
+
JSON.stringify(
|
|
8426
|
+
{
|
|
8427
|
+
ok: true,
|
|
8428
|
+
request_id: requestId,
|
|
8429
|
+
job_id: firstNonEmptyString(data.job_id, data.id) || null,
|
|
8430
|
+
answer: data.answer || "",
|
|
8431
|
+
context_blocks: Array.isArray(data.context_blocks) ? data.context_blocks : [],
|
|
8432
|
+
telemetry: data.telemetry || null,
|
|
8433
|
+
},
|
|
8434
|
+
null,
|
|
8435
|
+
2,
|
|
8436
|
+
),
|
|
8437
|
+
);
|
|
8438
|
+
return;
|
|
8439
|
+
}
|
|
8440
|
+
|
|
8441
|
+
console.log("Answer:\n", data.answer || "(no answer)");
|
|
6932
8442
|
if (data.context_blocks?.length) console.log(`\nContext blocks: ${data.context_blocks.length}`);
|
|
6933
8443
|
if (data.telemetry) {
|
|
6934
8444
|
const t = data.telemetry;
|
|
@@ -6942,8 +8452,8 @@ async function runChat(args) {
|
|
|
6942
8452
|
process.exit(1);
|
|
6943
8453
|
}
|
|
6944
8454
|
|
|
6945
|
-
async function main() {
|
|
6946
|
-
loadEnv();
|
|
8455
|
+
async function main() {
|
|
8456
|
+
const envPath = loadEnv();
|
|
6947
8457
|
|
|
6948
8458
|
const { command, rest } = splitCommand(process.argv.slice(2));
|
|
6949
8459
|
if (REMOVED_COMMAND_MESSAGES[command]) {
|
|
@@ -6955,12 +8465,27 @@ async function main() {
|
|
|
6955
8465
|
process.exit(1);
|
|
6956
8466
|
}
|
|
6957
8467
|
const args = parseArgs(rest);
|
|
6958
|
-
if (args.help || command === "help") {
|
|
8468
|
+
if (args.help || command === "help") {
|
|
6959
8469
|
printHelp();
|
|
6960
8470
|
return;
|
|
6961
|
-
}
|
|
6962
|
-
|
|
6963
|
-
if (command === "
|
|
8471
|
+
}
|
|
8472
|
+
|
|
8473
|
+
if (command === "version") {
|
|
8474
|
+
await runVersion(args);
|
|
8475
|
+
return;
|
|
8476
|
+
}
|
|
8477
|
+
|
|
8478
|
+
if (command === "info") {
|
|
8479
|
+
await runInfo(args, envPath);
|
|
8480
|
+
return;
|
|
8481
|
+
}
|
|
8482
|
+
|
|
8483
|
+
if (command === "doctor") {
|
|
8484
|
+
await runDoctor(args, envPath);
|
|
8485
|
+
return;
|
|
8486
|
+
}
|
|
8487
|
+
|
|
8488
|
+
if (command === "config") {
|
|
6964
8489
|
await runConfig(args);
|
|
6965
8490
|
return;
|
|
6966
8491
|
}
|
|
@@ -7040,12 +8565,19 @@ if (require.main === module) {
|
|
|
7040
8565
|
process.exit(1);
|
|
7041
8566
|
});
|
|
7042
8567
|
} else {
|
|
7043
|
-
module.exports = {
|
|
7044
|
-
|
|
7045
|
-
|
|
8568
|
+
module.exports = {
|
|
8569
|
+
buildFetchRuntime,
|
|
8570
|
+
buildMissionOpsPayload,
|
|
8571
|
+
classifyNetworkFailure,
|
|
8572
|
+
createHttpResponseError,
|
|
8573
|
+
createNetworkRequestError,
|
|
8574
|
+
collectCreateDraftPayload,
|
|
7046
8575
|
collectReviewPayload,
|
|
7047
8576
|
collectRevisionPayload,
|
|
7048
|
-
normalizeItemsPayload,
|
|
8577
|
+
normalizeItemsPayload,
|
|
8578
|
+
proxyEnvironmentSummary,
|
|
8579
|
+
probeDns,
|
|
8580
|
+
publicRequestFailure,
|
|
7049
8581
|
preserveMissionOpsWorkspace,
|
|
7050
8582
|
pruneLegacyCommandCenterArtifacts,
|
|
7051
8583
|
readYamlFile,
|
|
@@ -7057,6 +8589,8 @@ if (require.main === module) {
|
|
|
7057
8589
|
writeFeedbackSnapshot,
|
|
7058
8590
|
writeApprovedMissionCards,
|
|
7059
8591
|
writeMissionOpsSnapshot,
|
|
7060
|
-
writeQaqcSnapshot,
|
|
7061
|
-
|
|
7062
|
-
|
|
8592
|
+
writeQaqcSnapshot,
|
|
8593
|
+
fetchJsonWithTimeout,
|
|
8594
|
+
withTransientQueryRetries,
|
|
8595
|
+
};
|
|
8596
|
+
}
|