@mytegroupinc/myte-core 0.0.46 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -43
- package/cli.js +1896 -430
- package/lib/certification-manifest.js +178 -0
- package/package.json +7 -4
- package/scripts/feedback-certification-cleanup.js +53 -0
- package/scripts/feedback-live-full-harness.js +929 -122
- package/scripts/project-assistant-read-certification.js +258 -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,14 +24,16 @@ 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.",
|
|
29
|
-
"create-prds": "The `create-prds` alias has been removed. Use `myte create-prd <file.md> [more.md ...]
|
|
30
|
-
"add-prd": "The `add-prd` alias has been removed. Use `myte create-prd <file.md> [more.md ...]
|
|
31
|
-
prd: "The `prd` alias has been removed. Use `myte create-prd <file.md> [more.md ...]
|
|
34
|
+
"create-prds": "The `create-prds` alias has been removed. Use `myte create-prd <file.md> [more.md ...] --confirm-write --approval-artifact <path>`.",
|
|
35
|
+
"add-prd": "The `add-prd` alias has been removed. Use `myte create-prd <file.md> [more.md ...] --confirm-write --approval-artifact <path>`.",
|
|
36
|
+
prd: "The `prd` alias has been removed. Use `myte create-prd <file.md> [more.md ...] --confirm-write --approval-artifact <path>`.",
|
|
32
37
|
"qaqc-status-update": "The `qaqc-status-update` command has been removed. Use `myte mission status --mission-ids \"M001,M002\" --status todo|in_progress|done`.",
|
|
33
38
|
"mission-restore": "The project-key `myte mission restore` command has been removed. Restore archived missions from the Myte web archived missions board.",
|
|
34
39
|
};
|
|
@@ -45,11 +50,11 @@ function findEnvPath(startDir) {
|
|
|
45
50
|
return null;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
function loadEnv() {
|
|
49
|
-
const envPath = findEnvPath(process.cwd());
|
|
50
|
-
if (!envPath || !fs.existsSync(envPath)) return;
|
|
53
|
+
function loadEnv() {
|
|
54
|
+
const envPath = findEnvPath(process.cwd());
|
|
55
|
+
if (!envPath || !fs.existsSync(envPath)) return null;
|
|
51
56
|
const content = fs.readFileSync(envPath, "utf8");
|
|
52
|
-
content.split(/\r?\n/).forEach((line) => {
|
|
57
|
+
content.split(/\r?\n/).forEach((line) => {
|
|
53
58
|
const trimmed = String(line || "").trim();
|
|
54
59
|
if (!trimmed || trimmed.startsWith("#")) return;
|
|
55
60
|
const idx = trimmed.indexOf("=");
|
|
@@ -63,10 +68,11 @@ function loadEnv() {
|
|
|
63
68
|
val = val.slice(1, -1);
|
|
64
69
|
}
|
|
65
70
|
if (key && !(key in process.env)) {
|
|
66
|
-
process.env[key] = val;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
71
|
+
process.env[key] = val;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return envPath;
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
function splitCommand(argv) {
|
|
72
78
|
if (argv[0] === "mission") {
|
|
@@ -89,8 +95,13 @@ function splitCommand(argv) {
|
|
|
89
95
|
"query",
|
|
90
96
|
"ai",
|
|
91
97
|
"ask",
|
|
92
|
-
"chat",
|
|
93
|
-
"config",
|
|
98
|
+
"chat",
|
|
99
|
+
"config",
|
|
100
|
+
"doctor",
|
|
101
|
+
"info",
|
|
102
|
+
"version",
|
|
103
|
+
"--version",
|
|
104
|
+
"-v",
|
|
94
105
|
"bootstrap",
|
|
95
106
|
"suggestions",
|
|
96
107
|
"run-qaqc",
|
|
@@ -112,8 +123,13 @@ function splitCommand(argv) {
|
|
|
112
123
|
"-h",
|
|
113
124
|
]);
|
|
114
125
|
const first = argv[0];
|
|
115
|
-
if (first && known.has(first)) {
|
|
116
|
-
const cmd =
|
|
126
|
+
if (first && known.has(first)) {
|
|
127
|
+
const cmd =
|
|
128
|
+
first === "--help" || first === "-h"
|
|
129
|
+
? "help"
|
|
130
|
+
: first === "--version" || first === "-v"
|
|
131
|
+
? "version"
|
|
132
|
+
: first;
|
|
117
133
|
return { command: cmd, rest: argv.slice(1) };
|
|
118
134
|
}
|
|
119
135
|
return { command: "query", rest: argv };
|
|
@@ -181,45 +197,48 @@ 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]",
|
|
190
209
|
" myte mission archive --mission-ids \"M001[,M002...]\" [--reason \"...\"] [--no-sync] [--json]",
|
|
191
210
|
" myte sync-qaqc [--output-dir ./MyteCommandCenter] [--json]",
|
|
192
211
|
" myte suggestions sync [--output-dir ./MyteCommandCenter] [--json]",
|
|
193
|
-
" myte suggestions create [--file ./payload.yml] [--no-sync] [--json]",
|
|
194
|
-
" myte suggestions revise [--file ./payload.yml] [--no-sync] [--json]",
|
|
195
|
-
" myte suggestions review [--file ./payload.yml] [--no-sync] [--json]",
|
|
196
|
-
" myte update-team \"<content>\" [--json]",
|
|
197
|
-
" myte update-owner --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--json]",
|
|
198
|
-
" myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] [--json]",
|
|
212
|
+
" myte suggestions create [--file ./payload.yml] --confirm-write --approval-artifact <path> [--no-sync] [--json]",
|
|
213
|
+
" myte suggestions revise [--file ./payload.yml] --confirm-write --approval-artifact <path> [--no-sync] [--json]",
|
|
214
|
+
" myte suggestions review [--file ./payload.yml] [--no-sync] [--json]",
|
|
215
|
+
" myte update-team \"<content>\" --confirm-write --approval-artifact <path> [--json]",
|
|
216
|
+
" myte update-owner --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] --confirm-write --approval-artifact <path> [--json]",
|
|
217
|
+
" myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] --confirm-write --approval-artifact <path> [--json]",
|
|
199
218
|
" myte feedback-sync [--status <value>] [--source <value>] [--with-prd-text|--no-with-prd-text] [--output-dir ./MyteCommandCenter] [--json]",
|
|
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
|
-
" myte feedback submit --file ./MyteCommandCenter/reviews/feedback/<id>-edit.yml [--json]",
|
|
207
|
-
" myte feedback revise --request-id <id> --file ./MyteCommandCenter/reviews/feedback/<id>-edit.yml [--json]",
|
|
225
|
+
" myte feedback submit --file ./MyteCommandCenter/reviews/feedback/<id>-edit.yml --confirm-write --approval-artifact <path> [--json]",
|
|
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
|
-
" myte feedback comment --feedback-id <id> --body \"...\" [--body-file ./comment.md] [--json]",
|
|
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 ...] [--json] [--title \"...\"] [--description \"...\"]",
|
|
220
|
-
" cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
|
|
221
|
-
" cat update.md | myte update-owner --stdin --subject \"Owner update\"",
|
|
222
|
-
" cat update.md | myte update-client --stdin --subject \"Weekly client update\"",
|
|
238
|
+
" myte create-prd <file.md> [more.md ...] [--document-set] --confirm-write --approval-artifact <path> [--json] [--title \"...\"] [--description \"...\"]",
|
|
239
|
+
" cat file.md | myte create-prd --stdin --confirm-write --approval-artifact <path> [--title \"...\"] [--description \"...\"]",
|
|
240
|
+
" cat update.md | myte update-owner --stdin --subject \"Owner update\" --confirm-write --approval-artifact ./update.md",
|
|
241
|
+
" cat update.md | myte update-client --stdin --subject \"Weekly client update\" --confirm-write --approval-artifact ./update.md",
|
|
223
242
|
"",
|
|
224
243
|
"Run forms:",
|
|
225
244
|
" npm install myte then npx myte query \"...\" --with-diff",
|
|
@@ -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",
|
|
@@ -327,12 +348,25 @@ function printHelp() {
|
|
|
327
348
|
" - comment creates text-only feedback-specific comments; attachments remain web UI only for this endpoint",
|
|
328
349
|
" - project-key API/CLI cannot unarchive archived Feedback; use the web archived Feedback view",
|
|
329
350
|
" - get/history read current feedback snapshots and audited event history through the project-key API",
|
|
330
|
-
" - prd-versions/prd-diff expose retained PRD versions and backend-generated text diffs",
|
|
331
|
-
" -
|
|
332
|
-
" -
|
|
333
|
-
" -
|
|
334
|
-
"",
|
|
335
|
-
"
|
|
351
|
+
" - prd-versions/prd-diff expose retained PRD versions and backend-generated per-document text diffs",
|
|
352
|
+
" - edit/refine with --prd-file updates a review artifact only; --document-id targets one document in a PRD document set",
|
|
353
|
+
" - validate/apply send artifacts to /api/project-assistant/feedback/<id>/refinement/* for validation or owner-direct apply",
|
|
354
|
+
" - The backend owns authorization, stale snapshot checks, allowed field/transition rules, and history",
|
|
355
|
+
" - apply is idempotent and does not rewrite local feedback.yml; run feedback-sync after apply to refresh local state",
|
|
356
|
+
"",
|
|
357
|
+
"approval artifact requirements:",
|
|
358
|
+
" - Required: create-prd, update-team, update-owner, update-client, feedback submit/revise/comment, suggestions create/revise",
|
|
359
|
+
" - Not required: run-qaqc, mission status/archive, suggestions review, feedback review/move/undo/apply",
|
|
360
|
+
" - Never required: read/sync/query/validate commands, --print-context, --dry-run, and feedback local draft commands",
|
|
361
|
+
" - For required actions, prepare a .md/.markdown/.yml/.yaml/.json artifact, show it to the user, then rerun with --confirm-write --approval-artifact <path>",
|
|
362
|
+
" - In plain language, the artifact is the human-reviewed record of the exact write the agent is about to send; it is not a permission grant and does not bypass owner/delegate checks, idempotency, stale-state guards, validation, or audit",
|
|
363
|
+
" - Minimum artifact contents: exact command; every target id or PRD item; complete proposed-change summary; reason/approval note; batch_count when one command sends multiple items",
|
|
364
|
+
" - Mission edit/create batches use suggestions create/revise and must enumerate each item: update items include change_type: update, mission_id, and changed fields; create items include change_type: create, title, description, and acceptance details; revisions include suggestion_id and expected_revision",
|
|
365
|
+
" - PRD batches use one create-prd fileA.md fileB.md ...; list each PRD file/title/client_ref and make batch_count match the number of PRDs",
|
|
366
|
+
" - Feedback submit/revise/comment artifacts are usually the same YAML/markdown file being submitted; for inline text or stdin, use an approval memo with feedback_id/request_id and exact content",
|
|
367
|
+
" - Team, owner, and client update artifacts should name the audience and include the exact message body or complete summary; client updates should include target contact ids when used",
|
|
368
|
+
"",
|
|
369
|
+
"feedback action map:",
|
|
336
370
|
" - feedback-sync: pull board/PRD context into MyteCommandCenter for IDE work",
|
|
337
371
|
" - get: read one feedback item's current server snapshot and snapshot_hash",
|
|
338
372
|
" - status/edit/assign/archive/refine: create reviewable local YAML artifacts; no live mutation",
|
|
@@ -352,7 +386,9 @@ function printHelp() {
|
|
|
352
386
|
"Options:",
|
|
353
387
|
" --with-diff Include deterministic git diffs (project-scoped; fails fast if no project repos are configured or resolved)",
|
|
354
388
|
" --diff-limit <chars> Truncate diff context to N chars (default: 500000)",
|
|
355
|
-
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
389
|
+
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
390
|
+
" --verbose Print sanitized transport and retry diagnostics",
|
|
391
|
+
" --request-id <id> Stable, non-secret query request/idempotency identifier",
|
|
356
392
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
357
393
|
" --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
|
|
358
394
|
" --json-response Ask the Myte AI gateway to return clean JSON only and send OpenAI-compatible response_format",
|
|
@@ -361,8 +397,9 @@ function printHelp() {
|
|
|
361
397
|
" --output-dir <path> Command Center output directory (default: <current-workspace>/MyteCommandCenter)",
|
|
362
398
|
" --file <path> YAML/JSON payload file for suggestions create/revise/review",
|
|
363
399
|
" --stdin Read supported command content from stdin instead of inline text or a file path",
|
|
364
|
-
" --title <text> Override PRD title for raw markdown uploads",
|
|
365
|
-
" --description <text> Set feedback description/card summary for raw markdown uploads",
|
|
400
|
+
" --title <text> Override PRD title for raw markdown uploads",
|
|
401
|
+
" --description <text> Set feedback description/card summary for raw markdown uploads",
|
|
402
|
+
" --document-set Create one PRD feedback item containing one to three ordered markdown files",
|
|
366
403
|
" --content <text> Team update content for update-team",
|
|
367
404
|
" --body <text> Feedback comment body for feedback comment",
|
|
368
405
|
" --subject <text> Subject for update-owner or update-client",
|
|
@@ -375,8 +412,10 @@ function printHelp() {
|
|
|
375
412
|
" --request-id <id> Feedback review request ObjectId for submit/revise/review flows",
|
|
376
413
|
" --request-ids <ids> Comma-separated Feedback review request ObjectIds for batch review",
|
|
377
414
|
" --event-id <id> Feedback board event ObjectId for undo",
|
|
378
|
-
" --version-id <id> Feedback PRD version ObjectId for prd-diff",
|
|
379
|
-
" --compare-to <id> Optional base PRD version ObjectId for prd-diff",
|
|
415
|
+
" --version-id <id> Feedback PRD version ObjectId for prd-diff",
|
|
416
|
+
" --compare-to <id> Optional base PRD version ObjectId for prd-diff",
|
|
417
|
+
" --document-id <id> Stable PRD document id for document refinement or version diff",
|
|
418
|
+
" --prd-file <path> Read proposed PRD document Markdown into a local feedback review artifact",
|
|
380
419
|
" --action <value> Feedback review action: approve, reject, request_changes, or cancel",
|
|
381
420
|
" --to-state <value> Target canonical feedback board state for feedback move",
|
|
382
421
|
" --from-state <value> Optional current-state guard for feedback move",
|
|
@@ -392,8 +431,10 @@ function printHelp() {
|
|
|
392
431
|
" --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
|
|
393
432
|
" --mission-ids <ids> Comma-separated mission business ids for run-qaqc, mission status, or mission archive (quote multi-id values on PowerShell)",
|
|
394
433
|
" --reason <text> Optional reason for governed mission archive or feedback changes",
|
|
395
|
-
" --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
|
|
396
|
-
" --
|
|
434
|
+
" --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
|
|
435
|
+
" --confirm-write Attach approval metadata for commands that require an approval artifact",
|
|
436
|
+
" --approval-artifact Local .md/.markdown/.yml/.yaml/.json approval artifact for gated write commands",
|
|
437
|
+
" --wait Poll batch status until terminal completion for run-qaqc",
|
|
397
438
|
" --sync After run-qaqc completes, refresh local QAQC file",
|
|
398
439
|
" --force Allow run-qaqc to bypass stale-state protection when supported",
|
|
399
440
|
" --no-sync Skip automatic post-mutation sync for suggestions, missions, and feedback mutations",
|
|
@@ -401,38 +442,42 @@ function printHelp() {
|
|
|
401
442
|
" --no-fetch Don't git fetch origin main/master before diff",
|
|
402
443
|
"",
|
|
403
444
|
"Examples:",
|
|
404
|
-
" myte query \"What changed in logging?\" --with-diff",
|
|
445
|
+
" myte query \"What changed in logging?\" --with-diff",
|
|
446
|
+
" myte doctor --json",
|
|
447
|
+
" myte info --json",
|
|
448
|
+
" myte --version --verbose",
|
|
405
449
|
" myte ai \"Explain what this repository does\"",
|
|
406
450
|
" myte ai \"Return a JSON object with risks and next_steps\" --json-response",
|
|
407
451
|
" myte bootstrap",
|
|
408
452
|
" myte suggestions sync",
|
|
409
|
-
" myte suggestions create",
|
|
410
|
-
" myte suggestions revise --no-sync",
|
|
453
|
+
" myte suggestions create --file ./suggestions/create.yml --confirm-write --approval-artifact ./suggestions/create.yml",
|
|
454
|
+
" myte suggestions revise --file ./suggestions/revise.yml --confirm-write --approval-artifact ./suggestions/revise.yml --no-sync",
|
|
411
455
|
" myte suggestions review --file ./review.yml",
|
|
412
456
|
" myte run-qaqc --mission-ids \"M001,M002\" --wait --sync",
|
|
413
457
|
" myte mission status --mission-ids \"M001,M002\" --status done",
|
|
414
458
|
" myte mission archive --mission-ids \"M001\" --reason \"Duplicate disposable test mission\"",
|
|
415
459
|
" myte bootstrap --output-dir ./MyteCommandCenter",
|
|
416
460
|
" myte sync-qaqc",
|
|
417
|
-
" myte update-team \"Backend deploy completed; QAQC rerun queued.\"",
|
|
418
|
-
" myte update-owner --subject \"QAQC progress\" --body-file ./updates/owner.md",
|
|
419
|
-
" myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md",
|
|
461
|
+
" myte update-team \"Backend deploy completed; QAQC rerun queued.\" --confirm-write --approval-artifact ./updates/team.md",
|
|
462
|
+
" myte update-owner --subject \"QAQC progress\" --body-file ./updates/owner.md --confirm-write --approval-artifact ./updates/owner.md",
|
|
463
|
+
" myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md --confirm-write --approval-artifact ./updates/week-12.md",
|
|
420
464
|
" myte feedback-sync --json",
|
|
421
465
|
" myte feedback status --feedback-id 507f1f77bcf86cd799439011 --status in_review --reason \"Ready for owner review\"",
|
|
422
|
-
" myte feedback submit --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-edit.yml --json",
|
|
466
|
+
" myte feedback submit --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-edit.yml --confirm-write --approval-artifact ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-edit.yml --json",
|
|
423
467
|
" myte feedback reviews --status open --json",
|
|
424
468
|
" myte feedback review --request-id 507f1f77bcf86cd799439022 --action approve --reason \"Looks correct\" --json",
|
|
425
469
|
" myte feedback move --feedback-id 507f1f77bcf86cd799439011 --to-state in_progress --reason \"Started\" --json",
|
|
426
|
-
" myte feedback comment --feedback-id 507f1f77bcf86cd799439011 --body
|
|
470
|
+
" myte feedback comment --feedback-id 507f1f77bcf86cd799439011 --body-file ./comments/implementation.md --confirm-write --approval-artifact ./comments/implementation.md --json",
|
|
427
471
|
" myte feedback validate --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
|
|
428
472
|
" myte feedback apply --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
|
|
429
|
-
" myte suggestions create --file ./suggestions/create.yml",
|
|
430
|
-
" myte suggestions revise",
|
|
473
|
+
" myte suggestions create --file ./suggestions/create.yml --confirm-write --approval-artifact ./suggestions/create.yml",
|
|
474
|
+
" myte suggestions revise --file ./suggestions/revise.yml --confirm-write --approval-artifact ./suggestions/revise.yml",
|
|
431
475
|
" myte suggestions review",
|
|
432
|
-
" myte update-client --subject \"Weekly client update\" --body-markdown \"## Progress\\n- Login complete\" --target-contact-ids 507f1f77bcf86cd799439011,507f1f77bcf86cd799439012",
|
|
433
|
-
" myte create-prd ./drafts/auth-prd.md --description \"Short card summary\"",
|
|
434
|
-
" myte create-prd ./drafts/auth-prd.md ./drafts/billing-prd.md",
|
|
435
|
-
"
|
|
476
|
+
" myte update-client --subject \"Weekly client update\" --body-markdown \"## Progress\\n- Login complete\" --target-contact-ids 507f1f77bcf86cd799439011,507f1f77bcf86cd799439012 --confirm-write --approval-artifact ./updates/week-12.md",
|
|
477
|
+
" myte create-prd ./drafts/auth-prd.md --description \"Short card summary\" --confirm-write --approval-artifact ./drafts/auth-prd.md",
|
|
478
|
+
" myte create-prd ./drafts/auth-prd.md ./drafts/billing-prd.md --confirm-write --approval-artifact ./drafts/prd-batch.md",
|
|
479
|
+
" myte create-prd ./drafts/overview.md ./drafts/api.md ./drafts/web.md --document-set --title \"Platform PRD\" --confirm-write --approval-artifact ./drafts/platform-prd-approval.md",
|
|
480
|
+
" cat ./drafts/auth-prd.md | myte create-prd --stdin --confirm-write --approval-artifact ./drafts/auth-prd.md",
|
|
436
481
|
" myte config",
|
|
437
482
|
].join("\n");
|
|
438
483
|
console.log(text);
|
|
@@ -655,14 +700,280 @@ function parseFeedbackRequestIdsArg(args) {
|
|
|
655
700
|
);
|
|
656
701
|
}
|
|
657
702
|
|
|
658
|
-
function sleep(ms) {
|
|
659
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
703
|
+
function sleep(ms) {
|
|
704
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function proxyEnvironmentValue(env, lowerName, upperName) {
|
|
708
|
+
const lowerValue = String(env?.[lowerName] || "").trim();
|
|
709
|
+
if (lowerValue) return lowerValue;
|
|
710
|
+
return String(env?.[upperName] || "").trim();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function proxyEnvironmentSummary(env = process.env) {
|
|
714
|
+
const httpProxy = proxyEnvironmentValue(env, "http_proxy", "HTTP_PROXY");
|
|
715
|
+
const httpsProxy = proxyEnvironmentValue(env, "https_proxy", "HTTPS_PROXY");
|
|
716
|
+
const noProxy = proxyEnvironmentValue(env, "no_proxy", "NO_PROXY");
|
|
717
|
+
return {
|
|
718
|
+
configured: Boolean(httpProxy || httpsProxy),
|
|
719
|
+
http_proxy_present: Boolean(httpProxy),
|
|
720
|
+
https_proxy_present: Boolean(httpsProxy),
|
|
721
|
+
no_proxy_present: Boolean(noProxy),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function buildFetchRuntime({
|
|
726
|
+
env = process.env,
|
|
727
|
+
nativeFetch = globalThis.fetch,
|
|
728
|
+
undiciModule,
|
|
729
|
+
} = {}) {
|
|
730
|
+
const proxySummary = proxyEnvironmentSummary(env);
|
|
731
|
+
if (!proxySummary.configured) {
|
|
732
|
+
if (typeof nativeFetch !== "function") {
|
|
733
|
+
const error = new Error("Global fetch is unavailable. myte requires Node 18.17+.");
|
|
734
|
+
error.code = "MYTE_FETCH_UNAVAILABLE";
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
fetch: nativeFetch,
|
|
739
|
+
transport: "node_fetch",
|
|
740
|
+
proxy: proxySummary,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
let undici = undiciModule;
|
|
745
|
+
if (!undici) {
|
|
746
|
+
try {
|
|
747
|
+
undici = require("undici");
|
|
748
|
+
} catch (cause) {
|
|
749
|
+
const error = new Error(
|
|
750
|
+
"Proxy variables are configured, but the Myte HTTP transport is unavailable. Reinstall the myte package.",
|
|
751
|
+
);
|
|
752
|
+
error.code = "MYTE_PROXY_TRANSPORT_UNAVAILABLE";
|
|
753
|
+
error.cause = cause;
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (typeof undici.fetch !== "function" || typeof undici.EnvHttpProxyAgent !== "function") {
|
|
758
|
+
const error = new Error(
|
|
759
|
+
"The installed Myte HTTP transport does not support HTTP_PROXY, HTTPS_PROXY, and NO_PROXY.",
|
|
760
|
+
);
|
|
761
|
+
error.code = "MYTE_PROXY_TRANSPORT_INCOMPATIBLE";
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const dispatcher = new undici.EnvHttpProxyAgent({
|
|
766
|
+
httpProxy: proxyEnvironmentValue(env, "http_proxy", "HTTP_PROXY") || undefined,
|
|
767
|
+
httpsProxy: proxyEnvironmentValue(env, "https_proxy", "HTTPS_PROXY") || undefined,
|
|
768
|
+
noProxy: proxyEnvironmentValue(env, "no_proxy", "NO_PROXY") || undefined,
|
|
769
|
+
});
|
|
770
|
+
return {
|
|
771
|
+
fetch: (url, options = {}) => undici.fetch(url, { ...options, dispatcher }),
|
|
772
|
+
transport: "undici_env_proxy",
|
|
773
|
+
proxy: proxySummary,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
let fetchRuntimePromise = null;
|
|
778
|
+
|
|
779
|
+
async function getFetchRuntime() {
|
|
780
|
+
if (!fetchRuntimePromise) {
|
|
781
|
+
fetchRuntimePromise = Promise.resolve().then(() => buildFetchRuntime());
|
|
782
|
+
}
|
|
783
|
+
return fetchRuntimePromise;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function getFetch() {
|
|
787
|
+
return (await getFetchRuntime()).fetch;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function requestErrorChain(error) {
|
|
791
|
+
const chain = [];
|
|
792
|
+
const seen = new Set();
|
|
793
|
+
let current = error;
|
|
794
|
+
while (current && typeof current === "object" && !seen.has(current) && chain.length < 8) {
|
|
795
|
+
seen.add(current);
|
|
796
|
+
chain.push(current);
|
|
797
|
+
current = current.cause;
|
|
798
|
+
}
|
|
799
|
+
return chain;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function requestErrorCode(error) {
|
|
803
|
+
for (const item of requestErrorChain(error)) {
|
|
804
|
+
const code = String(item?.code || item?.errno || "").trim();
|
|
805
|
+
if (code) return code;
|
|
806
|
+
}
|
|
807
|
+
return "";
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function requestErrorMessage(error) {
|
|
811
|
+
return requestErrorChain(error)
|
|
812
|
+
.map((item) => String(item?.message || "").trim())
|
|
813
|
+
.filter(Boolean)
|
|
814
|
+
.join(" ");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function safeEndpointHost(url) {
|
|
818
|
+
try {
|
|
819
|
+
return new URL(String(url || "")).host || "unknown";
|
|
820
|
+
} catch {
|
|
821
|
+
return "unknown";
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function classifyNetworkFailure(error, { timedOut = false } = {}) {
|
|
826
|
+
const code = timedOut ? "ETIMEDOUT" : requestErrorCode(error) || "NETWORK_ERROR";
|
|
827
|
+
const message = requestErrorMessage(error).toLowerCase();
|
|
828
|
+
const certificateFailure =
|
|
829
|
+
/^(CERT_|ERR_TLS_CERT_|DEPTH_ZERO_SELF_SIGNED_CERT|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE)/.test(code) ||
|
|
830
|
+
/certificate|self[- ]signed|unable to verify|hostname\/ip does not match/.test(message);
|
|
831
|
+
|
|
832
|
+
if (timedOut || code === "ABORT_ERR" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ETIMEDOUT") {
|
|
833
|
+
return {
|
|
834
|
+
kind: "timeout",
|
|
835
|
+
layer: "network",
|
|
836
|
+
code,
|
|
837
|
+
detail: "The request exceeded its configured timeout before a complete HTTP response was received.",
|
|
838
|
+
guidance: "Retry on a stable network or increase --timeout-ms when the API is known to be reachable.",
|
|
839
|
+
transient: true,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
843
|
+
return {
|
|
844
|
+
kind: "dns",
|
|
845
|
+
layer: "dns",
|
|
846
|
+
code,
|
|
847
|
+
detail: "The API hostname could not be resolved.",
|
|
848
|
+
guidance: "Check DNS connectivity, captive-portal login, VPN, or local resolver settings.",
|
|
849
|
+
transient: true,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
if (code === "ECONNREFUSED") {
|
|
853
|
+
return {
|
|
854
|
+
kind: "connection_refused",
|
|
855
|
+
layer: "tcp",
|
|
856
|
+
code,
|
|
857
|
+
detail: "The remote host refused the TCP connection.",
|
|
858
|
+
guidance: "Check the API base, proxy/VPN path, firewall policy, and service availability.",
|
|
859
|
+
transient: true,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (certificateFailure) {
|
|
863
|
+
return {
|
|
864
|
+
kind: "tls_certificate",
|
|
865
|
+
layer: "tls",
|
|
866
|
+
code,
|
|
867
|
+
detail: "TLS certificate validation failed.",
|
|
868
|
+
guidance: "Check system time, trusted corporate certificates, proxy interception, and the requested hostname. Do not disable TLS verification.",
|
|
869
|
+
transient: false,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
if (
|
|
873
|
+
code === "ECONNRESET" ||
|
|
874
|
+
code === "UND_ERR_SOCKET" ||
|
|
875
|
+
/secure tls connection|tls connection|socket disconnected|connection reset|other side closed/.test(message)
|
|
876
|
+
) {
|
|
877
|
+
return {
|
|
878
|
+
kind: "connection_reset",
|
|
879
|
+
layer: "tls",
|
|
880
|
+
code,
|
|
881
|
+
detail: /before secure tls|secure tls connection/.test(message)
|
|
882
|
+
? "The TLS connection was reset before the secure connection was established."
|
|
883
|
+
: "The network connection was reset before a complete HTTP response was received.",
|
|
884
|
+
guidance: "The API key was loaded, but the request did not reach the Myte API. Check captive-portal login, restricted Wi-Fi, proxy/VPN settings, or retry on another network.",
|
|
885
|
+
transient: true,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (/ssl|tls|handshake/.test(message) || code.startsWith("ERR_SSL")) {
|
|
889
|
+
return {
|
|
890
|
+
kind: "tls",
|
|
891
|
+
layer: "tls",
|
|
892
|
+
code,
|
|
893
|
+
detail: "The TLS handshake failed before an HTTP response was received.",
|
|
894
|
+
guidance: "Check captive-portal login, proxy/VPN interception, system trust, or retry on another network.",
|
|
895
|
+
transient: true,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
kind: "network",
|
|
900
|
+
layer: "network",
|
|
901
|
+
code,
|
|
902
|
+
detail: "The transport failed before a complete HTTP response was received.",
|
|
903
|
+
guidance: "Run `myte doctor --json`, then check network, proxy/VPN, captive-portal, and API base settings.",
|
|
904
|
+
transient: true,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function createNetworkRequestError(error, url, { timedOut = false } = {}) {
|
|
909
|
+
if (error?.isMyteRequestError) return error;
|
|
910
|
+
const classified = classifyNetworkFailure(error, { timedOut });
|
|
911
|
+
const endpoint = safeEndpointHost(url);
|
|
912
|
+
const wrapped = new Error(
|
|
913
|
+
[
|
|
914
|
+
"Network request failed before receiving an HTTP response.",
|
|
915
|
+
`Endpoint: ${endpoint}`,
|
|
916
|
+
`Cause: ${classified.code}`,
|
|
917
|
+
`Detail: ${classified.detail}`,
|
|
918
|
+
classified.guidance,
|
|
919
|
+
].join("\n"),
|
|
920
|
+
);
|
|
921
|
+
wrapped.name = "MyteNetworkError";
|
|
922
|
+
wrapped.code = classified.code;
|
|
923
|
+
wrapped.failureKind = classified.kind;
|
|
924
|
+
wrapped.failureLayer = classified.layer;
|
|
925
|
+
wrapped.endpoint = endpoint;
|
|
926
|
+
wrapped.detail = classified.detail;
|
|
927
|
+
wrapped.guidance = classified.guidance;
|
|
928
|
+
wrapped.transient = classified.transient;
|
|
929
|
+
wrapped.isMyteRequestError = true;
|
|
930
|
+
wrapped.cause = error;
|
|
931
|
+
return wrapped;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function createNonJsonResponseError(resp, url) {
|
|
935
|
+
const status = Number(resp?.status || 0);
|
|
936
|
+
const contentType = String(resp?.headers?.get?.("content-type") || "").toLowerCase();
|
|
937
|
+
const endpoint = safeEndpointHost(url);
|
|
938
|
+
const likelyPortal = status >= 200 && status < 400 && contentType.includes("text/html");
|
|
939
|
+
const detail = likelyPortal
|
|
940
|
+
? "The endpoint returned HTML instead of JSON, which commonly indicates a captive portal or intercepting proxy."
|
|
941
|
+
: "The endpoint returned a non-JSON response, so the CLI could not validate the API contract.";
|
|
942
|
+
const error = new Error(
|
|
943
|
+
[
|
|
944
|
+
`Invalid HTTP response (${status || "unknown status"}).`,
|
|
945
|
+
`Endpoint: ${endpoint}`,
|
|
946
|
+
`Cause: ${likelyPortal ? "CAPTIVE_PORTAL_OR_PROXY" : "NON_JSON_RESPONSE"}`,
|
|
947
|
+
`Detail: ${detail}`,
|
|
948
|
+
].join("\n"),
|
|
949
|
+
);
|
|
950
|
+
error.name = "MyteHttpError";
|
|
951
|
+
error.code = likelyPortal ? "CAPTIVE_PORTAL_OR_PROXY" : "NON_JSON_RESPONSE";
|
|
952
|
+
error.failureKind = likelyPortal ? "captive_portal" : "non_json_response";
|
|
953
|
+
error.failureLayer = "http";
|
|
954
|
+
error.endpoint = endpoint;
|
|
955
|
+
error.status = status;
|
|
956
|
+
error.transient = likelyPortal || [408, 429, 500, 502, 503, 504].includes(status);
|
|
957
|
+
error.isMyteRequestError = true;
|
|
958
|
+
const retryAfter = resp?.headers?.get?.("retry-after");
|
|
959
|
+
const requestId = resp?.headers?.get?.("x-request-id");
|
|
960
|
+
if (retryAfter) error.retryAfter = retryAfter;
|
|
961
|
+
if (requestId) error.requestId = String(requestId);
|
|
962
|
+
return error;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function publicRequestFailure(error) {
|
|
966
|
+
return {
|
|
967
|
+
layer: error?.failureLayer || (error?.status ? "http" : "unknown"),
|
|
968
|
+
kind: error?.failureKind || "unknown",
|
|
969
|
+
code: String(error?.code || error?.status || "UNKNOWN"),
|
|
970
|
+
status: Number(error?.status || 0) || null,
|
|
971
|
+
endpoint: error?.endpoint || null,
|
|
972
|
+
detail: error?.detail || String(error?.message || "Request failed.").split("\n")[0],
|
|
973
|
+
transient: Boolean(error?.transient),
|
|
974
|
+
request_id: error?.requestId || null,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
666
977
|
|
|
667
978
|
function normalizeApiBase(baseRaw) {
|
|
668
979
|
const baseTrim = String(baseRaw || "").trim().replace(/\/+$/, "");
|
|
@@ -670,24 +981,31 @@ function normalizeApiBase(baseRaw) {
|
|
|
670
981
|
return base.endsWith("/api") ? base : `${base}/api`;
|
|
671
982
|
}
|
|
672
983
|
|
|
673
|
-
async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
|
|
674
|
-
const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
throw
|
|
689
|
-
}
|
|
690
|
-
|
|
984
|
+
async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
|
|
985
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
|
|
986
|
+
let timedOut = false;
|
|
987
|
+
const timeoutId =
|
|
988
|
+
controller && timeoutMs > 0
|
|
989
|
+
? setTimeout(() => {
|
|
990
|
+
timedOut = true;
|
|
991
|
+
controller.abort();
|
|
992
|
+
}, timeoutMs)
|
|
993
|
+
: undefined;
|
|
994
|
+
try {
|
|
995
|
+
let resp;
|
|
996
|
+
try {
|
|
997
|
+
resp = await fetchFn(url, { ...options, signal: controller?.signal });
|
|
998
|
+
} catch (error) {
|
|
999
|
+
throw createNetworkRequestError(error, url, { timedOut });
|
|
1000
|
+
}
|
|
1001
|
+
const text = await resp.text();
|
|
1002
|
+
let body;
|
|
1003
|
+
try {
|
|
1004
|
+
body = JSON.parse(text);
|
|
1005
|
+
} catch {
|
|
1006
|
+
throw createNonJsonResponseError(resp, url);
|
|
1007
|
+
}
|
|
1008
|
+
return { resp, body };
|
|
691
1009
|
} finally {
|
|
692
1010
|
if (timeoutId) clearTimeout(timeoutId);
|
|
693
1011
|
}
|
|
@@ -1621,14 +1939,11 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
|
1621
1939
|
headers: { Authorization: `Bearer ${key}` },
|
|
1622
1940
|
},
|
|
1623
1941
|
timeoutMs
|
|
1624
|
-
);
|
|
1625
|
-
|
|
1626
|
-
if (!resp.ok || body.status !== "success") {
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
err.status = resp.status;
|
|
1630
|
-
throw err;
|
|
1631
|
-
}
|
|
1942
|
+
);
|
|
1943
|
+
|
|
1944
|
+
if (!resp.ok || body.status !== "success") {
|
|
1945
|
+
throw createHttpResponseError(resp, body, url, "Project configuration could not be loaded.");
|
|
1946
|
+
}
|
|
1632
1947
|
return body.data || {};
|
|
1633
1948
|
}
|
|
1634
1949
|
|
|
@@ -1682,7 +1997,7 @@ async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
|
|
|
1682
1997
|
return body.data || {};
|
|
1683
1998
|
}
|
|
1684
1999
|
|
|
1685
|
-
async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {} }) {
|
|
2000
|
+
async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {} }) {
|
|
1686
2001
|
const fetchFn = await getFetch();
|
|
1687
2002
|
const url = new URL(`${apiBase}/project-assistant/feedback-sync`);
|
|
1688
2003
|
if (filters.status) url.searchParams.set("status", String(filters.status));
|
|
@@ -1690,9 +2005,21 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
|
|
|
1690
2005
|
if (filters.includePrdText !== undefined) {
|
|
1691
2006
|
url.searchParams.set("include_prd_text", filters.includePrdText ? "true" : "false");
|
|
1692
2007
|
}
|
|
1693
|
-
if (filters.includeCommentTurns !== undefined) {
|
|
1694
|
-
url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
|
|
1695
|
-
}
|
|
2008
|
+
if (filters.includeCommentTurns !== undefined) {
|
|
2009
|
+
url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
|
|
2010
|
+
}
|
|
2011
|
+
if (filters.limit !== undefined && filters.limit !== null && filters.limit !== "") {
|
|
2012
|
+
url.searchParams.set("limit", String(filters.limit));
|
|
2013
|
+
}
|
|
2014
|
+
if (filters.offset !== undefined && filters.offset !== null && Number(filters.offset) > 0) {
|
|
2015
|
+
url.searchParams.set("offset", String(filters.offset));
|
|
2016
|
+
}
|
|
2017
|
+
if (filters.paginationMode) {
|
|
2018
|
+
url.searchParams.set("pagination_mode", String(filters.paginationMode));
|
|
2019
|
+
}
|
|
2020
|
+
if (filters.cursor) {
|
|
2021
|
+
url.searchParams.set("cursor", String(filters.cursor));
|
|
2022
|
+
}
|
|
1696
2023
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
1697
2024
|
fetchFn,
|
|
1698
2025
|
url.toString(),
|
|
@@ -1709,8 +2036,191 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
|
|
|
1709
2036
|
err.status = resp.status;
|
|
1710
2037
|
throw err;
|
|
1711
2038
|
}
|
|
1712
|
-
return body.data || {};
|
|
1713
|
-
}
|
|
2039
|
+
return body.data || {};
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function resolveBoundedInteger(value, { fallback, min, max, label }) {
|
|
2043
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
2044
|
+
const parsed = Number(value);
|
|
2045
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
2046
|
+
throw new Error(`${label} must be an integer between ${min} and ${max}.`);
|
|
2047
|
+
}
|
|
2048
|
+
return parsed;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function aggregateFeedbackSyncCounts(items, availableTotal) {
|
|
2052
|
+
const byStatus = {};
|
|
2053
|
+
const bySource = {};
|
|
2054
|
+
let withPrdText = 0;
|
|
2055
|
+
let withConversationTurns = 0;
|
|
2056
|
+
for (const item of items) {
|
|
2057
|
+
const status = String(item?.status || "Pending");
|
|
2058
|
+
const source = String(item?.source || "User");
|
|
2059
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
2060
|
+
bySource[source] = (bySource[source] || 0) + 1;
|
|
2061
|
+
if (String(item?.prd_text || "").trim()) withPrdText += 1;
|
|
2062
|
+
if (Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0) {
|
|
2063
|
+
withConversationTurns += 1;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return {
|
|
2067
|
+
total_feedback: items.length,
|
|
2068
|
+
available_feedback: Number.isFinite(Number(availableTotal)) ? Number(availableTotal) : items.length,
|
|
2069
|
+
with_prd_text: withPrdText,
|
|
2070
|
+
with_conversation_turns: withConversationTurns,
|
|
2071
|
+
by_status: byStatus,
|
|
2072
|
+
by_source: bySource,
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
async function fetchCompleteFeedbackSyncSnapshot({
|
|
2077
|
+
apiBase,
|
|
2078
|
+
key,
|
|
2079
|
+
timeoutMs,
|
|
2080
|
+
filters = {},
|
|
2081
|
+
pageSize = DEFAULT_FEEDBACK_SYNC_PAGE_SIZE,
|
|
2082
|
+
maxItems = 0,
|
|
2083
|
+
onPage,
|
|
2084
|
+
}) {
|
|
2085
|
+
const items = [];
|
|
2086
|
+
const pageHashes = [];
|
|
2087
|
+
const stageTotals = {};
|
|
2088
|
+
let firstSnapshot = null;
|
|
2089
|
+
let offset = 0;
|
|
2090
|
+
let availableTotal = null;
|
|
2091
|
+
let pageCount = 0;
|
|
2092
|
+
let sawPagination = false;
|
|
2093
|
+
let cursor = null;
|
|
2094
|
+
let useCursorPagination = true;
|
|
2095
|
+
let serverHasMore = false;
|
|
2096
|
+
const seenFeedbackIds = new Set();
|
|
2097
|
+
|
|
2098
|
+
while (true) {
|
|
2099
|
+
const remaining = maxItems > 0 ? maxItems - items.length : pageSize;
|
|
2100
|
+
if (maxItems > 0 && remaining <= 0) break;
|
|
2101
|
+
const requestedLimit = Math.min(pageSize, maxItems > 0 ? remaining : pageSize);
|
|
2102
|
+
const page = await fetchFeedbackSyncSnapshot({
|
|
2103
|
+
apiBase,
|
|
2104
|
+
key,
|
|
2105
|
+
timeoutMs,
|
|
2106
|
+
filters: {
|
|
2107
|
+
...filters,
|
|
2108
|
+
limit: requestedLimit,
|
|
2109
|
+
...(useCursorPagination
|
|
2110
|
+
? {
|
|
2111
|
+
paginationMode: "cursor",
|
|
2112
|
+
...(cursor ? { cursor } : {}),
|
|
2113
|
+
}
|
|
2114
|
+
: { offset }),
|
|
2115
|
+
},
|
|
2116
|
+
});
|
|
2117
|
+
pageCount += 1;
|
|
2118
|
+
if (!firstSnapshot) firstSnapshot = page;
|
|
2119
|
+
|
|
2120
|
+
const pageItems = Array.isArray(page.items) ? page.items : [];
|
|
2121
|
+
const pagination = page.pagination && typeof page.pagination === "object" ? page.pagination : null;
|
|
2122
|
+
sawPagination = sawPagination || Boolean(pagination);
|
|
2123
|
+
if (
|
|
2124
|
+
availableTotal === null &&
|
|
2125
|
+
pagination &&
|
|
2126
|
+
Number.isFinite(Number(pagination.total))
|
|
2127
|
+
) {
|
|
2128
|
+
availableTotal = Number(pagination.total);
|
|
2129
|
+
}
|
|
2130
|
+
if (page.snapshot_hash) pageHashes.push(String(page.snapshot_hash));
|
|
2131
|
+
const stages = page?.diagnostics?.stages_ms;
|
|
2132
|
+
if (stages && typeof stages === "object") {
|
|
2133
|
+
for (const [name, value] of Object.entries(stages)) {
|
|
2134
|
+
const duration = Number(value);
|
|
2135
|
+
if (Number.isFinite(duration)) stageTotals[name] = (stageTotals[name] || 0) + duration;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
for (const item of pageItems) {
|
|
2140
|
+
const feedbackId = firstNonEmptyString(item?.feedback_id, item?._id);
|
|
2141
|
+
if (feedbackId && seenFeedbackIds.has(feedbackId)) {
|
|
2142
|
+
throw new Error(
|
|
2143
|
+
`Feedback sync snapshot changed while paging; duplicate feedback ${feedbackId} was returned. Retry the sync.`,
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
if (feedbackId) seenFeedbackIds.add(feedbackId);
|
|
2147
|
+
items.push(item);
|
|
2148
|
+
}
|
|
2149
|
+
if (typeof onPage === "function") {
|
|
2150
|
+
onPage({
|
|
2151
|
+
page: pageCount,
|
|
2152
|
+
received: pageItems.length,
|
|
2153
|
+
synced: items.length,
|
|
2154
|
+
total: availableTotal,
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (!pagination) break;
|
|
2159
|
+
const responseUsesCursor = String(pagination.mode || "").toLowerCase() === "cursor";
|
|
2160
|
+
if (!responseUsesCursor) useCursorPagination = false;
|
|
2161
|
+
const hasMore = Boolean(pagination.has_more);
|
|
2162
|
+
serverHasMore = hasMore;
|
|
2163
|
+
if (!hasMore) break;
|
|
2164
|
+
if (pageItems.length === 0) {
|
|
2165
|
+
throw new Error(`Feedback sync page ${pageCount} reported more results but returned no items.`);
|
|
2166
|
+
}
|
|
2167
|
+
if (responseUsesCursor) {
|
|
2168
|
+
const nextCursor = firstNonEmptyString(pagination.next_cursor);
|
|
2169
|
+
if (!nextCursor || nextCursor === cursor) {
|
|
2170
|
+
throw new Error(
|
|
2171
|
+
`Feedback sync page ${pageCount} did not return a new cursor while more results remain.`,
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
cursor = nextCursor;
|
|
2175
|
+
useCursorPagination = true;
|
|
2176
|
+
} else {
|
|
2177
|
+
useCursorPagination = false;
|
|
2178
|
+
offset += pageItems.length;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
const base = firstSnapshot || {};
|
|
2183
|
+
if (!sawPagination && pageCount === 1) {
|
|
2184
|
+
return {
|
|
2185
|
+
...base,
|
|
2186
|
+
diagnostics: {
|
|
2187
|
+
...(base.diagnostics && typeof base.diagnostics === "object" ? base.diagnostics : {}),
|
|
2188
|
+
page_count: 1,
|
|
2189
|
+
},
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
const aggregateHash = createHash("sha256")
|
|
2193
|
+
.update(JSON.stringify({
|
|
2194
|
+
project_id: base?.project?.id || null,
|
|
2195
|
+
item_hashes: items.map((item) => item?.snapshot_hash || item?.feedback_id || null),
|
|
2196
|
+
page_hashes: pageHashes,
|
|
2197
|
+
}))
|
|
2198
|
+
.digest("hex");
|
|
2199
|
+
|
|
2200
|
+
return {
|
|
2201
|
+
...base,
|
|
2202
|
+
items,
|
|
2203
|
+
counts: aggregateFeedbackSyncCounts(items, availableTotal),
|
|
2204
|
+
pagination: {
|
|
2205
|
+
mode: useCursorPagination && sawPagination ? "cursor" : "offset",
|
|
2206
|
+
total: availableTotal ?? items.length,
|
|
2207
|
+
limit: pageSize,
|
|
2208
|
+
offset: 0,
|
|
2209
|
+
has_more:
|
|
2210
|
+
maxItems > 0
|
|
2211
|
+
? serverHasMore || (availableTotal !== null && items.length < availableTotal)
|
|
2212
|
+
: false,
|
|
2213
|
+
page_count: pageCount,
|
|
2214
|
+
synced_count: items.length,
|
|
2215
|
+
next_cursor: maxItems > 0 && serverHasMore ? cursor : null,
|
|
2216
|
+
},
|
|
2217
|
+
snapshot_hash: aggregateHash,
|
|
2218
|
+
diagnostics: {
|
|
2219
|
+
page_count: pageCount,
|
|
2220
|
+
stages_ms: stageTotals,
|
|
2221
|
+
},
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
1714
2224
|
|
|
1715
2225
|
async function fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId }) {
|
|
1716
2226
|
const fetchFn = await getFetch();
|
|
@@ -2055,10 +2565,19 @@ async function fetchFeedbackPrdVersions({ apiBase, key, timeoutMs, feedbackId })
|
|
|
2055
2565
|
return body.data || {};
|
|
2056
2566
|
}
|
|
2057
2567
|
|
|
2058
|
-
async function fetchFeedbackPrdVersionDiff({
|
|
2568
|
+
async function fetchFeedbackPrdVersionDiff({
|
|
2569
|
+
apiBase,
|
|
2570
|
+
key,
|
|
2571
|
+
timeoutMs,
|
|
2572
|
+
feedbackId,
|
|
2573
|
+
versionId,
|
|
2574
|
+
compareTo,
|
|
2575
|
+
documentId,
|
|
2576
|
+
}) {
|
|
2059
2577
|
const fetchFn = await getFetch();
|
|
2060
|
-
const url = new URL(`${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/prd/versions/${encodeURIComponent(String(versionId || ""))}/diff`);
|
|
2061
|
-
if (compareTo) url.searchParams.set("compare_to", String(compareTo));
|
|
2578
|
+
const url = new URL(`${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/prd/versions/${encodeURIComponent(String(versionId || ""))}/diff`);
|
|
2579
|
+
if (compareTo) url.searchParams.set("compare_to", String(compareTo));
|
|
2580
|
+
if (documentId) url.searchParams.set("document_id", String(documentId));
|
|
2062
2581
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
2063
2582
|
fetchFn,
|
|
2064
2583
|
url.toString(),
|
|
@@ -2270,48 +2789,162 @@ async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
|
|
|
2270
2789
|
return body.data || {};
|
|
2271
2790
|
}
|
|
2272
2791
|
|
|
2273
|
-
function resolveRetryAfterMs(err, fallbackMs = 5_000) {
|
|
2274
|
-
const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
|
|
2275
|
-
const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
|
|
2276
|
-
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
|
|
2277
|
-
return Math.min(retryAfterSeconds * 1_000, 60_000);
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
return
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2792
|
+
function resolveRetryAfterMs(err, fallbackMs = 5_000) {
|
|
2793
|
+
const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
|
|
2794
|
+
const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
|
|
2795
|
+
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
|
|
2796
|
+
return Math.min(retryAfterSeconds * 1_000, 60_000);
|
|
2797
|
+
}
|
|
2798
|
+
const retryAfterDate = Date.parse(String(retryAfterRaw || ""));
|
|
2799
|
+
if (Number.isFinite(retryAfterDate)) {
|
|
2800
|
+
return Math.min(Math.max(0, retryAfterDate - Date.now()), 60_000);
|
|
2801
|
+
}
|
|
2802
|
+
return fallbackMs;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function isTransientQueryStatusError(err) {
|
|
2806
|
+
return Boolean(
|
|
2807
|
+
err?.transient ||
|
|
2808
|
+
["dns", "tcp", "tls", "network"].includes(String(err?.failureLayer || "")) ||
|
|
2809
|
+
[408, 429, 500, 502, 503, 504].includes(Number(err?.status)),
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
function createHttpResponseError(resp, body, url, fallbackMessage) {
|
|
2814
|
+
const status = Number(resp?.status || 0);
|
|
2815
|
+
const endpoint = safeEndpointHost(url);
|
|
2816
|
+
let kind = "http";
|
|
2817
|
+
let detail = fallbackMessage || `HTTP request failed (${status || "unknown status"}).`;
|
|
2818
|
+
let transient = false;
|
|
2819
|
+
if (status === 401) {
|
|
2820
|
+
kind = "authentication";
|
|
2821
|
+
detail = "The API rejected the project key.";
|
|
2822
|
+
} else if (status === 403) {
|
|
2823
|
+
kind = "authorization";
|
|
2824
|
+
detail = "The authenticated project identity is not allowed to perform this action.";
|
|
2825
|
+
} else if (status === 429) {
|
|
2826
|
+
kind = "rate_limit";
|
|
2827
|
+
detail = "The API rate limit is temporarily exhausted.";
|
|
2828
|
+
transient = true;
|
|
2829
|
+
} else if ([408, 500, 502, 503, 504].includes(status)) {
|
|
2830
|
+
kind = "server";
|
|
2831
|
+
detail = "The API or an upstream gateway returned a transient error.";
|
|
2832
|
+
transient = true;
|
|
2833
|
+
}
|
|
2834
|
+
const requestId =
|
|
2835
|
+
resp?.headers?.get?.("x-request-id") ||
|
|
2836
|
+
(body && typeof body === "object" ? body.request_id || body?.data?.request_id : null);
|
|
2837
|
+
const error = new Error(
|
|
2838
|
+
[
|
|
2839
|
+
`HTTP request failed (${status || "unknown status"}).`,
|
|
2840
|
+
`Endpoint: ${endpoint}`,
|
|
2841
|
+
`Cause: ${kind.toUpperCase()}`,
|
|
2842
|
+
`Detail: ${detail}`,
|
|
2843
|
+
requestId ? `Request ID: ${requestId}` : "",
|
|
2844
|
+
]
|
|
2845
|
+
.filter(Boolean)
|
|
2846
|
+
.join("\n"),
|
|
2847
|
+
);
|
|
2848
|
+
error.name = "MyteHttpError";
|
|
2849
|
+
error.code = `HTTP_${status || "ERROR"}`;
|
|
2850
|
+
error.status = status;
|
|
2851
|
+
error.failureKind = kind;
|
|
2852
|
+
error.failureLayer = "http";
|
|
2853
|
+
error.endpoint = endpoint;
|
|
2854
|
+
error.detail = detail;
|
|
2855
|
+
error.transient = transient;
|
|
2856
|
+
error.isMyteRequestError = true;
|
|
2857
|
+
if (requestId) error.requestId = String(requestId);
|
|
2858
|
+
const retryAfter = resp?.headers?.get?.("retry-after");
|
|
2859
|
+
if (retryAfter) error.retryAfter = retryAfter;
|
|
2860
|
+
return error;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function queryRequestId(args) {
|
|
2864
|
+
const explicit = firstNonEmptyString(
|
|
2865
|
+
args["request-id"],
|
|
2866
|
+
args.requestId,
|
|
2867
|
+
args.request_id,
|
|
2868
|
+
args["idempotency-key"],
|
|
2869
|
+
args.idempotencyKey,
|
|
2870
|
+
args.idempotency_key,
|
|
2871
|
+
);
|
|
2872
|
+
const value = explicit || `myte-query-${randomUUID()}`;
|
|
2873
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
|
|
2874
|
+
throw new Error("request-id must contain 1-128 safe letters, numbers, dots, underscores, colons, or hyphens.");
|
|
2875
|
+
}
|
|
2876
|
+
return value;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function transientRetryDelayMs(error, attempt, initialDelayMs = 750) {
|
|
2880
|
+
const exponential = Math.min(15_000, initialDelayMs * 2 ** Math.max(0, attempt - 1));
|
|
2881
|
+
const fallback = resolveRetryAfterMs(error, exponential);
|
|
2882
|
+
const jitterLimit = Math.max(0, Number(process.env.MYTE_RETRY_JITTER_MS ?? 250) || 0);
|
|
2883
|
+
const jitter = jitterLimit ? Math.floor(Math.random() * (jitterLimit + 1)) : 0;
|
|
2884
|
+
return Math.min(60_000, fallback + jitter);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
async function withTransientQueryRetries(
|
|
2888
|
+
operation,
|
|
2889
|
+
{
|
|
2890
|
+
maxAttempts = 3,
|
|
2891
|
+
verbose = false,
|
|
2892
|
+
label = "request",
|
|
2893
|
+
initialDelayMs = 750,
|
|
2894
|
+
} = {},
|
|
2895
|
+
) {
|
|
2896
|
+
let lastError;
|
|
2897
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2898
|
+
try {
|
|
2899
|
+
return await operation(attempt);
|
|
2900
|
+
} catch (error) {
|
|
2901
|
+
lastError = error;
|
|
2902
|
+
if (!isTransientQueryStatusError(error) || attempt >= maxAttempts) throw error;
|
|
2903
|
+
const waitMs = transientRetryDelayMs(error, attempt, initialDelayMs);
|
|
2904
|
+
if (verbose) {
|
|
2905
|
+
console.error(
|
|
2906
|
+
`[myte] ${label} transient failure (${error?.code || error?.status || "unknown"}); retry ${attempt + 1}/${maxAttempts} in ${waitMs}ms.`,
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2909
|
+
await sleep(waitMs);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
throw lastError;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
async function createAssistantQueryJob({
|
|
2916
|
+
apiBase,
|
|
2917
|
+
key,
|
|
2918
|
+
payload,
|
|
2919
|
+
timeoutMs,
|
|
2920
|
+
requestId,
|
|
2921
|
+
endpoint = "/project-assistant/query",
|
|
2922
|
+
}) {
|
|
2923
|
+
const fetchFn = await getFetch();
|
|
2924
|
+
const url = `${apiBase}${endpoint}`;
|
|
2925
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
2290
2926
|
fetchFn,
|
|
2291
2927
|
url,
|
|
2292
2928
|
{
|
|
2293
2929
|
method: "POST",
|
|
2294
2930
|
headers: {
|
|
2295
|
-
"Content-Type": "application/json",
|
|
2296
|
-
Authorization: `Bearer ${key}`,
|
|
2297
|
-
|
|
2298
|
-
|
|
2931
|
+
"Content-Type": "application/json",
|
|
2932
|
+
Authorization: `Bearer ${key}`,
|
|
2933
|
+
"X-Idempotency-Key": requestId,
|
|
2934
|
+
"X-Request-ID": requestId,
|
|
2935
|
+
},
|
|
2936
|
+
body: JSON.stringify({ ...payload, client_request_id: requestId }),
|
|
2299
2937
|
},
|
|
2300
2938
|
timeoutMs
|
|
2301
2939
|
);
|
|
2302
2940
|
|
|
2303
|
-
if (!resp.ok || body.status !== "success") {
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
}
|
|
2311
|
-
return body.data || {};
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId }) {
|
|
2941
|
+
if (!resp.ok || body.status !== "success") {
|
|
2942
|
+
throw createHttpResponseError(resp, body, url, "The query job could not be created.");
|
|
2943
|
+
}
|
|
2944
|
+
return body.data || {};
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId, requestId }) {
|
|
2315
2948
|
const fetchFn = await getFetch();
|
|
2316
2949
|
const url = `${apiBase}/project-assistant/query/${encodeURIComponent(String(jobId || ""))}`;
|
|
2317
2950
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
@@ -2319,19 +2952,17 @@ async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId })
|
|
|
2319
2952
|
url,
|
|
2320
2953
|
{
|
|
2321
2954
|
method: "GET",
|
|
2322
|
-
headers: {
|
|
2955
|
+
headers: {
|
|
2956
|
+
Authorization: `Bearer ${key}`,
|
|
2957
|
+
...(requestId ? { "X-Request-ID": requestId } : {}),
|
|
2958
|
+
},
|
|
2323
2959
|
},
|
|
2324
2960
|
timeoutMs
|
|
2325
2961
|
);
|
|
2326
2962
|
|
|
2327
|
-
if (!resp.ok || body.status !== "success") {
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
|
|
2331
|
-
err.status = resp.status;
|
|
2332
|
-
if (retryAfter) err.retryAfter = retryAfter;
|
|
2333
|
-
throw err;
|
|
2334
|
-
}
|
|
2963
|
+
if (!resp.ok || body.status !== "success") {
|
|
2964
|
+
throw createHttpResponseError(resp, body, url, "The query status could not be loaded.");
|
|
2965
|
+
}
|
|
2335
2966
|
return body.data || {};
|
|
2336
2967
|
}
|
|
2337
2968
|
|
|
@@ -2355,11 +2986,6 @@ async function runRunQaqc(args) {
|
|
|
2355
2986
|
};
|
|
2356
2987
|
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
2357
2988
|
if (clientSessionId) payload.client_session_id = clientSessionId;
|
|
2358
|
-
payload = withWriteApproval(args, payload, {
|
|
2359
|
-
artifactKind: "yaml",
|
|
2360
|
-
targets: missionIds,
|
|
2361
|
-
batchCount: missionIds.length > 1 ? missionIds.length : null,
|
|
2362
|
-
});
|
|
2363
2989
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
2364
2990
|
args,
|
|
2365
2991
|
operation: "run-qaqc",
|
|
@@ -2515,11 +3141,6 @@ async function runMissionStatus(args) {
|
|
|
2515
3141
|
};
|
|
2516
3142
|
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
2517
3143
|
if (clientSessionId) payload.client_session_id = clientSessionId;
|
|
2518
|
-
payload = withWriteApproval(args, payload, {
|
|
2519
|
-
artifactKind: "yaml",
|
|
2520
|
-
targets: missionIds,
|
|
2521
|
-
batchCount: missionIds.length > 1 ? missionIds.length : null,
|
|
2522
|
-
});
|
|
2523
3144
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
2524
3145
|
args,
|
|
2525
3146
|
operation: "mission-status-update",
|
|
@@ -2624,15 +3245,9 @@ async function runMissionArchiveCommand(args) {
|
|
|
2624
3245
|
if (reason) payload.reason = reason;
|
|
2625
3246
|
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
2626
3247
|
if (clientSessionId) payload.client_session_id = clientSessionId;
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
batchCount: missionIds.length > 1 ? missionIds.length : null,
|
|
2631
|
-
});
|
|
2632
|
-
|
|
2633
|
-
const operation = "mission-archive";
|
|
2634
|
-
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
2635
|
-
args,
|
|
3248
|
+
const operation = "mission-archive";
|
|
3249
|
+
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
3250
|
+
args,
|
|
2636
3251
|
operation,
|
|
2637
3252
|
payload,
|
|
2638
3253
|
});
|
|
@@ -3099,7 +3714,7 @@ function resolveWriteApprovalArtifact(args) {
|
|
|
3099
3714
|
}
|
|
3100
3715
|
const ext = path.extname(absPath).toLowerCase();
|
|
3101
3716
|
if (![".md", ".markdown", ".yml", ".yaml", ".json"].includes(ext)) {
|
|
3102
|
-
console.error("Approval artifact must be a local .md, .yml, .yaml, or .json file.");
|
|
3717
|
+
console.error("Approval artifact must be a local .md, .markdown, .yml, .yaml, or .json file.");
|
|
3103
3718
|
process.exit(1);
|
|
3104
3719
|
}
|
|
3105
3720
|
return { displayPath: String(artifactPath), absPath };
|
|
@@ -3109,8 +3724,20 @@ function approvalTargets(values) {
|
|
|
3109
3724
|
return uniqueNormalizedStrings(Array.isArray(values) ? values : [values]);
|
|
3110
3725
|
}
|
|
3111
3726
|
|
|
3112
|
-
function
|
|
3727
|
+
function hasWriteApprovalArtifactArg(args) {
|
|
3728
|
+
return Boolean(firstNonEmptyString(
|
|
3729
|
+
args["approval-artifact"],
|
|
3730
|
+
args.approvalArtifact,
|
|
3731
|
+
args.approval_artifact
|
|
3732
|
+
));
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
function withWriteApproval(args, payload, { artifactKind = "yaml", targets = [], batchCount = null, required = true } = {}) {
|
|
3113
3736
|
const confirmed = Boolean(args["confirm-write"] || args.confirmWrite || args.confirm_write);
|
|
3737
|
+
if (required && (!confirmed || !hasWriteApprovalArtifactArg(args))) {
|
|
3738
|
+
console.error("Missing --confirm-write --approval-artifact <path> for this live Myte write.");
|
|
3739
|
+
process.exit(1);
|
|
3740
|
+
}
|
|
3114
3741
|
if (!confirmed) return payload;
|
|
3115
3742
|
const { displayPath } = resolveWriteApprovalArtifact(args);
|
|
3116
3743
|
const nextPayload = { ...(payload || {}) };
|
|
@@ -3268,12 +3895,50 @@ function writeJsonFile(filePath, value) {
|
|
|
3268
3895
|
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
3269
3896
|
}
|
|
3270
3897
|
|
|
3271
|
-
function writeTextFile(filePath, value) {
|
|
3272
|
-
ensureDir(path.dirname(filePath));
|
|
3273
|
-
fs.writeFileSync(filePath, String(value || ""), "utf8");
|
|
3274
|
-
}
|
|
3275
|
-
|
|
3276
|
-
function
|
|
3898
|
+
function writeTextFile(filePath, value) {
|
|
3899
|
+
ensureDir(path.dirname(filePath));
|
|
3900
|
+
fs.writeFileSync(filePath, String(value || ""), "utf8");
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
function replacePathsTransaction(entries, token) {
|
|
3904
|
+
const prepared = [];
|
|
3905
|
+
try {
|
|
3906
|
+
for (const entry of entries) {
|
|
3907
|
+
if (!entry?.staged || !entry?.target || !fs.existsSync(entry.staged)) {
|
|
3908
|
+
throw new Error(`Missing staged sync artifact for ${entry?.target || "unknown target"}.`);
|
|
3909
|
+
}
|
|
3910
|
+
ensureDir(path.dirname(entry.target));
|
|
3911
|
+
const backup = `${entry.target}.myte-backup-${token}`;
|
|
3912
|
+
if (fs.existsSync(backup)) removePathIfExists(backup);
|
|
3913
|
+
if (fs.existsSync(entry.target)) fs.renameSync(entry.target, backup);
|
|
3914
|
+
prepared.push({ ...entry, backup, installed: false });
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
for (const entry of prepared) {
|
|
3918
|
+
fs.renameSync(entry.staged, entry.target);
|
|
3919
|
+
entry.installed = true;
|
|
3920
|
+
}
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
for (const entry of [...prepared].reverse()) {
|
|
3923
|
+
if (entry.installed && fs.existsSync(entry.target)) removePathIfExists(entry.target);
|
|
3924
|
+
if (fs.existsSync(entry.backup)) fs.renameSync(entry.backup, entry.target);
|
|
3925
|
+
}
|
|
3926
|
+
throw error;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
for (const entry of prepared) {
|
|
3930
|
+
if (!fs.existsSync(entry.backup)) continue;
|
|
3931
|
+
try {
|
|
3932
|
+
removePathIfExists(entry.backup);
|
|
3933
|
+
} catch (error) {
|
|
3934
|
+
console.warn(
|
|
3935
|
+
`[myte] Sync committed, but an old backup could not be removed: ${path.basename(entry.backup)}`,
|
|
3936
|
+
);
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
function sanitizeFileSegment(value, fallback = "item") {
|
|
3277
3942
|
const cleaned = String(value || "")
|
|
3278
3943
|
.trim()
|
|
3279
3944
|
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
|
|
@@ -3416,19 +4081,123 @@ function resolvePortableWorkspace(repoNames) {
|
|
|
3416
4081
|
};
|
|
3417
4082
|
}
|
|
3418
4083
|
|
|
3419
|
-
function resolveCommandCenterRoots(wrapperRoot, outputDir) {
|
|
3420
|
-
const targetRoot = outputDir
|
|
3421
|
-
? path.resolve(process.cwd(), String(outputDir))
|
|
3422
|
-
: path.join(wrapperRoot, "MyteCommandCenter");
|
|
3423
|
-
return {
|
|
3424
|
-
targetRoot,
|
|
3425
|
-
dataRoot: path.join(targetRoot, "data"),
|
|
3426
|
-
};
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
function
|
|
3430
|
-
|
|
3431
|
-
|
|
4084
|
+
function resolveCommandCenterRoots(wrapperRoot, outputDir) {
|
|
4085
|
+
const targetRoot = outputDir
|
|
4086
|
+
? path.resolve(process.cwd(), String(outputDir))
|
|
4087
|
+
: path.join(wrapperRoot, "MyteCommandCenter");
|
|
4088
|
+
return {
|
|
4089
|
+
targetRoot,
|
|
4090
|
+
dataRoot: path.join(targetRoot, "data"),
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
function buildAgentsMyteApiMarkdown() {
|
|
4095
|
+
return ensureTrailingNewline([
|
|
4096
|
+
"# AgentsMyteAPI",
|
|
4097
|
+
"",
|
|
4098
|
+
"This file is generated by `myte bootstrap` as contextual guidance for coding agents using a project-scoped Myte API key.",
|
|
4099
|
+
"It does not grant permission, approve a write, or replace project authorization. Prefer the `myte` CLI over direct HTTP.",
|
|
4100
|
+
"",
|
|
4101
|
+
"## Auth And Discovery",
|
|
4102
|
+
"",
|
|
4103
|
+
"- Use `MYTE_API_KEY` or `MYTE_PROJECT_API_KEY` for `/api/project-assistant/*`.",
|
|
4104
|
+
"- Default API base: `https://api.myte.dev/api`.",
|
|
4105
|
+
"- Start with `myte config --json`, then `myte bootstrap --json`.",
|
|
4106
|
+
"- Use local IDs from `MyteCommandCenter/data/missions/*.yml`, `data/mission-ops.yml`, and `data/feedback.yml`.",
|
|
4107
|
+
"- `myte ai` is intentionally outside this project-scoped table. It uses `MYTEAI_API_KEY` and the Myte AI gateway, not `/api/project-assistant/*`.",
|
|
4108
|
+
"",
|
|
4109
|
+
"## Approval Artifact Rule",
|
|
4110
|
+
"",
|
|
4111
|
+
"Only the commands marked `required` below need `--confirm-write --approval-artifact <path>`.",
|
|
4112
|
+
"Approval artifacts are local `.md`, `.markdown`, `.yml`, `.yaml`, or `.json` files that list the operation, exact target IDs, proposed change, and batch count when relevant.",
|
|
4113
|
+
"The artifact is the human-reviewed record of the intended mutation. It does not grant permission, bypass owner/delegate checks, or replace idempotency; it gives the API enough evidence that a human approved this exact write payload.",
|
|
4114
|
+
"",
|
|
4115
|
+
"Minimum artifact contents for required actions:",
|
|
4116
|
+
"",
|
|
4117
|
+
"- `command`: the exact CLI command you plan to run.",
|
|
4118
|
+
"- `targets`: every affected project, feedback, request, suggestion, mission, PRD file, client ref, or contact id.",
|
|
4119
|
+
"- `proposed_change`: a complete summary of the text/status/content being sent.",
|
|
4120
|
+
"- `batch_count`: required when the command sends more than one item in one API call.",
|
|
4121
|
+
"- `reason` or `approval_note`: why the write is being sent now.",
|
|
4122
|
+
"",
|
|
4123
|
+
"Batch guidance:",
|
|
4124
|
+
"",
|
|
4125
|
+
"- Mission edit/create batches use `myte suggestions create` or `myte suggestions revise`; the artifact must list each item. For mission edits include `change_type: update`, the existing `mission_id`, and the changed fields. For new missions include `change_type: create`, the proposed title, description, and acceptance details. For revisions include each `suggestion_id` and expected revision.",
|
|
4126
|
+
"- PRD batches use one `myte create-prd fileA.md fileB.md ...`; the artifact must list each PRD file/title/client ref. `batch_count` must match the number of PRD items.",
|
|
4127
|
+
"- Feedback submit/revise/comment artifacts are usually the same YAML or markdown file being submitted. If the command uses inline text or stdin, create a separate approval memo that includes the feedback id or request id and the exact content.",
|
|
4128
|
+
"- Team, owner, and client update artifacts should name the audience and include the exact message body or a complete summary; client updates should include target contact ids when used.",
|
|
4129
|
+
"- `feedback review --request-ids`, `feedback move --feedback-ids`, `mission status --mission-ids`, `mission archive --mission-ids`, and `run-qaqc --mission-ids` are batched governed writes, but they are marked `no` below and should not receive approval artifact flags.",
|
|
4130
|
+
"",
|
|
4131
|
+
"Example approval artifact:",
|
|
4132
|
+
"",
|
|
4133
|
+
"```yaml",
|
|
4134
|
+
"command: myte suggestions create --file ./mission-suggestions.yml --confirm-write --approval-artifact ./mission-suggestions-approval.yml",
|
|
4135
|
+
"reason: Human approved this mission review batch after inspecting the proposed changes.",
|
|
4136
|
+
"batch_count: 2",
|
|
4137
|
+
"targets:",
|
|
4138
|
+
" - mission_id: M093",
|
|
4139
|
+
" change_type: update",
|
|
4140
|
+
" proposed_change: Mark acceptance criteria complete and clarify final QA note.",
|
|
4141
|
+
" - new_mission: Add deployment verification",
|
|
4142
|
+
" change_type: create",
|
|
4143
|
+
" proposed_change: Create a mission to verify deploy logs, smoke test, and rollback note.",
|
|
4144
|
+
"```",
|
|
4145
|
+
"",
|
|
4146
|
+
"| Action | CLI shape | API route | Class | Artifact approval |",
|
|
4147
|
+
"|---|---|---|---|---|",
|
|
4148
|
+
"| Config | `myte config --json` | `GET /project-assistant/config` | read | no |",
|
|
4149
|
+
"| Query | `myte query \"...\" --with-diff` | `POST /project-assistant/query`; polls `GET /project-assistant/query/<job_id>` when queued; `--with-diff` also reads `GET /project-assistant/config` | query job | no |",
|
|
4150
|
+
"| Bootstrap | `myte bootstrap --json` | `GET /project-assistant/bootstrap` and `GET /project-assistant/suggestions` | local sync | no |",
|
|
4151
|
+
"| QAQC sync | `myte sync-qaqc --json` | `GET /project-assistant/qaqc-sync` | local sync | no |",
|
|
4152
|
+
"| Run QAQC | `myte run-qaqc --mission-ids \"M001,M002\" --wait --sync --json` | `POST /project-assistant/run-qaqc`; `--wait` polls `GET /project-assistant/run-qaqc/<batch_id>`; `--sync` reads `GET /project-assistant/qaqc-sync` | governed write | no |",
|
|
4153
|
+
"| Mission status | `myte mission status --mission-ids \"M001\" --status done --json` | `POST /project-assistant/mission-status-update` | governed write | no |",
|
|
4154
|
+
"| Mission archive | `myte mission archive --mission-ids \"M001\" --reason \"...\" --json` | `POST /project-assistant/mission-archive` | governed write | no |",
|
|
4155
|
+
"| Suggestions sync | `myte suggestions sync --json` | `GET /project-assistant/suggestions` | local sync | no |",
|
|
4156
|
+
"| Suggestions create | `myte suggestions create --file ./create.yml --confirm-write --approval-artifact ./create.yml --json` | `POST /project-assistant/suggestions` | review-thread write | required |",
|
|
4157
|
+
"| Suggestions revise | `myte suggestions revise --file ./revise.yml --confirm-write --approval-artifact ./revise.yml --json` | `POST /project-assistant/suggestions/revise` | review-thread write | required |",
|
|
4158
|
+
"| Suggestions review | `myte suggestions review --file ./review.yml --json` | `POST /project-assistant/suggestions/review` | governed review write | no |",
|
|
4159
|
+
"| Feedback sync | `myte feedback-sync --json` | `GET /project-assistant/feedback-sync` | local sync | no |",
|
|
4160
|
+
"| Feedback get | `myte feedback get --feedback-id <id> --json` | `GET /project-assistant/feedback/<id>` | read | no |",
|
|
4161
|
+
"| Feedback history | `myte feedback history --feedback-id <id> --json` | `GET /project-assistant/feedback/<id>/events` | read | no |",
|
|
4162
|
+
"| Feedback local drafts | `myte feedback status/edit/assign/archive/refine ...` | none | local artifact only | no |",
|
|
4163
|
+
"| Feedback validate | `myte feedback validate --file ./review.yml --json` | `POST /project-assistant/feedback/<id>/refinement/validate` | validation | no |",
|
|
4164
|
+
"| Feedback submit | `myte feedback submit --file ./review.yml --confirm-write --approval-artifact ./review.yml --json` | `POST /project-assistant/feedback/<id>/refinement/requests` | review-request write | required |",
|
|
4165
|
+
"| Feedback revise | `myte feedback revise --request-id <id> --file ./review.yml --confirm-write --approval-artifact ./review.yml --json` | `POST /project-assistant/feedback-review-requests/<id>/revise` | review-request write | required |",
|
|
4166
|
+
"| Feedback reviews | `myte feedback reviews [--status open/terminal/all] [--request-id <id>] --json` (`requests` alias) | `GET /project-assistant/feedback-review-requests` or `GET /project-assistant/feedback-review-requests/<id>` | read | no |",
|
|
4167
|
+
"| Feedback review | `myte feedback review --request-id <id> --action approve/reject/request_changes/cancel [--reason \"...\"] --json` | `POST /project-assistant/feedback-review-requests/<id>/review`, `/request-changes`, or `/cancel` | governed review write | no |",
|
|
4168
|
+
"| Feedback batch review | `myte feedback review --request-ids \"<id1,id2>\" --action approve/reject/request_changes/cancel --json` | `POST /project-assistant/feedback-review-requests/batch-review` | governed review write | no |",
|
|
4169
|
+
"| Feedback move | `myte feedback move --feedback-id <id> --to-state in_progress --reason \"...\" --json` | `POST /project-assistant/feedback/<id>/board-move` | governed write | no |",
|
|
4170
|
+
"| Feedback batch move | `myte feedback move --feedback-ids \"<id1,id2>\" --to-state in_progress --json` | `POST /project-assistant/feedback/batch-board-move` | governed write | no |",
|
|
4171
|
+
"| Feedback comment | `myte feedback comment --feedback-id <id> --body-file ./comment.md --confirm-write --approval-artifact ./comment.md --json` | `POST /project-assistant/feedback/<id>/comments` | comment write | required |",
|
|
4172
|
+
"| Feedback undo | `myte feedback undo --feedback-id <id> --event-id <id> --reason \"...\" --json` | `POST /project-assistant/feedback/<id>/events/<event_id>/undo` | governed write | no |",
|
|
4173
|
+
"| Feedback PRD versions | `myte feedback prd-versions --feedback-id <id> --json` | `GET /project-assistant/feedback/<id>/prd/versions` | read | no |",
|
|
4174
|
+
"| Feedback PRD diff | `myte feedback prd-diff --feedback-id <id> --version-id <id>` | `GET /project-assistant/feedback/<id>/prd/versions/<version_id>/diff` | read | no |",
|
|
4175
|
+
"| Feedback apply | `myte feedback apply --file ./review.yml --json` | `POST /project-assistant/feedback/<id>/refinement/apply` | governed write | no |",
|
|
4176
|
+
"| Create PRD | `myte create-prd ./prd.md --confirm-write --approval-artifact ./prd.md --json` | `POST /project-assistant/create-prd` | PRD write | required |",
|
|
4177
|
+
"| Batch create PRDs | `myte create-prd ./a.md ./b.md --confirm-write --approval-artifact ./approval.md --json` | `POST /project-assistant/create-prds` | PRD write | required |",
|
|
4178
|
+
"| Team update | `myte update-team \"...\" --confirm-write --approval-artifact ./team.md --json` | `POST /project-assistant/project-comment` | project comment | required |",
|
|
4179
|
+
"| Owner update | `myte update-owner --subject \"...\" --body-file ./owner.md --confirm-write --approval-artifact ./owner.md --json` | `POST /project-assistant/update-owner` | owner email | required |",
|
|
4180
|
+
"| Client update | `myte update-client --subject \"...\" --body-file ./client.md --confirm-write --approval-artifact ./client.md --json` | `POST /project-assistant/client-update-drafts` | client draft | required |",
|
|
4181
|
+
"",
|
|
4182
|
+
"## Agent Rules",
|
|
4183
|
+
"",
|
|
4184
|
+
"- For `required` actions, draft the artifact, show it to the user, get explicit approval, then rerun with the approval flags.",
|
|
4185
|
+
"- For `no` actions, do not add approval flags. The API still enforces key binding, project permissions, idempotency, stale-state checks, and route validation.",
|
|
4186
|
+
"- For local draft actions, inspect the generated YAML before submitting or applying it.",
|
|
4187
|
+
"- Use `--print-context` or `--dry-run` where available to inspect payloads without sending a write.",
|
|
4188
|
+
].join("\n"));
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
function writeAgentsMyteApiGuide({ wrapperRoot, outputDir }) {
|
|
4192
|
+
const { targetRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
|
|
4193
|
+
const guidePath = path.join(targetRoot, "AgentsMyteAPI.md");
|
|
4194
|
+
writeTextFile(guidePath, buildAgentsMyteApiMarkdown());
|
|
4195
|
+
return guidePath;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
function pruneLegacyCommandCenterArtifacts(dataRoot, options = {}) {
|
|
4199
|
+
const {
|
|
4200
|
+
bootstrap = false,
|
|
3432
4201
|
qaqc = false,
|
|
3433
4202
|
feedback = false,
|
|
3434
4203
|
missionOps = false,
|
|
@@ -3506,11 +4275,12 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
3506
4275
|
writeYamlFile(path.join(missionsDir, `${missionPathId}.yml`), mission);
|
|
3507
4276
|
});
|
|
3508
4277
|
|
|
3509
|
-
if (snapshot.project && typeof snapshot.project === "object") {
|
|
3510
|
-
writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
|
|
3511
|
-
}
|
|
3512
|
-
|
|
3513
|
-
|
|
4278
|
+
if (snapshot.project && typeof snapshot.project === "object") {
|
|
4279
|
+
writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
|
|
4280
|
+
}
|
|
4281
|
+
const agentsMyteApiPath = writeAgentsMyteApiGuide({ wrapperRoot, outputDir });
|
|
4282
|
+
|
|
4283
|
+
const manifest = {
|
|
3514
4284
|
schema_version: snapshot.schema_version || 1,
|
|
3515
4285
|
generated_at: snapshot.generated_at || null,
|
|
3516
4286
|
snapshot_hash: snapshot.snapshot_hash || null,
|
|
@@ -3523,12 +4293,13 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
3523
4293
|
missions: missions.length,
|
|
3524
4294
|
},
|
|
3525
4295
|
};
|
|
3526
|
-
return {
|
|
3527
|
-
targetRoot,
|
|
3528
|
-
dataRoot,
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
}
|
|
4296
|
+
return {
|
|
4297
|
+
targetRoot,
|
|
4298
|
+
dataRoot,
|
|
4299
|
+
agentsMyteApiPath,
|
|
4300
|
+
manifest,
|
|
4301
|
+
};
|
|
4302
|
+
}
|
|
3532
4303
|
|
|
3533
4304
|
function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
3534
4305
|
const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
|
|
@@ -3562,37 +4333,87 @@ function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
3562
4333
|
};
|
|
3563
4334
|
}
|
|
3564
4335
|
|
|
3565
|
-
function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
3566
|
-
const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
|
|
3567
|
-
const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
const
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
const
|
|
3582
|
-
const
|
|
3583
|
-
const
|
|
3584
|
-
const
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
let
|
|
3588
|
-
let
|
|
3589
|
-
let
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
4336
|
+
function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
4337
|
+
const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
|
|
4338
|
+
const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
|
|
4339
|
+
const transactionToken = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
4340
|
+
const stagingRoot = path.join(targetRoot, `.myte-feedback-sync-${transactionToken}`);
|
|
4341
|
+
const stagedDataRoot = path.join(stagingRoot, "data");
|
|
4342
|
+
const stagedPrdRoot = path.join(stagingRoot, "PRD", "feedback-sync");
|
|
4343
|
+
ensureDir(stagedDataRoot);
|
|
4344
|
+
ensureDir(stagedPrdRoot);
|
|
4345
|
+
|
|
4346
|
+
const items = Array.isArray(snapshot.items) ? snapshot.items : [];
|
|
4347
|
+
let prdFileCount = 0;
|
|
4348
|
+
let prdDocumentSetCount = 0;
|
|
4349
|
+
const materializedItems = items.map((rawItem, index) => {
|
|
4350
|
+
const item = isPlainObject(rawItem) ? { ...rawItem } : {};
|
|
4351
|
+
const feedbackId = stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`);
|
|
4352
|
+
const conversationTurns = normalizeFeedbackConversationTurns(item.conversation_turns);
|
|
4353
|
+
const prdText = String(item.prd_text || "").trim();
|
|
4354
|
+
const rawPrdDocuments = Array.isArray(item.prd_documents) ? item.prd_documents : [];
|
|
4355
|
+
const attachmentDocuments = Array.isArray(item.attachment_documents) ? item.attachment_documents : [];
|
|
4356
|
+
|
|
4357
|
+
let contextSource = "description_only";
|
|
4358
|
+
let contextNote = "No separate PRD. Use feedback_text as the context for this feedback item.";
|
|
4359
|
+
let prdFile = null;
|
|
4360
|
+
let materializedPrdDocuments = rawPrdDocuments.map((document) =>
|
|
4361
|
+
isPlainObject(document) ? { ...document } : {}
|
|
4362
|
+
);
|
|
4363
|
+
|
|
4364
|
+
if (rawPrdDocuments.length > 0) {
|
|
4365
|
+
prdDocumentSetCount += 1;
|
|
4366
|
+
const feedbackFolderName = sanitizeFileSegment(feedbackId, `feedback-${index + 1}`);
|
|
4367
|
+
materializedPrdDocuments = rawPrdDocuments.map((rawDocument, documentIndex) => {
|
|
4368
|
+
const document = isPlainObject(rawDocument) ? { ...rawDocument } : {};
|
|
4369
|
+
const markdown = String(
|
|
4370
|
+
document.canonical_markdown || document.source_markdown || ""
|
|
4371
|
+
);
|
|
4372
|
+
let localFile = null;
|
|
4373
|
+
if (markdown.trim()) {
|
|
4374
|
+
const rawFilename = sanitizeFileSegment(
|
|
4375
|
+
document.filename || document.title,
|
|
4376
|
+
`document-${documentIndex + 1}.md`
|
|
4377
|
+
);
|
|
4378
|
+
const filename = /\.(md|markdown)$/i.test(rawFilename)
|
|
4379
|
+
? rawFilename
|
|
4380
|
+
: `${rawFilename}.md`;
|
|
4381
|
+
const position = Number.isInteger(Number(document.position))
|
|
4382
|
+
? Number(document.position) + 1
|
|
4383
|
+
: documentIndex + 1;
|
|
4384
|
+
const orderedFilename = `${String(position).padStart(2, "0")}-${filename}`;
|
|
4385
|
+
const stagedPath = path.join(stagedPrdRoot, feedbackFolderName, orderedFilename);
|
|
4386
|
+
ensureDir(path.dirname(stagedPath));
|
|
4387
|
+
writeTextFile(stagedPath, ensureTrailingNewline(markdown));
|
|
4388
|
+
localFile = toPosixRelativePath(
|
|
4389
|
+
targetRoot,
|
|
4390
|
+
path.join(prdSyncDir, feedbackFolderName, orderedFilename)
|
|
4391
|
+
);
|
|
4392
|
+
prdFileCount += 1;
|
|
4393
|
+
}
|
|
4394
|
+
return {
|
|
4395
|
+
...document,
|
|
4396
|
+
local_file: localFile,
|
|
4397
|
+
};
|
|
4398
|
+
});
|
|
4399
|
+
const primaryDocumentId = String(item.primary_prd_document_id || "");
|
|
4400
|
+
const primaryDocument =
|
|
4401
|
+
materializedPrdDocuments.find(
|
|
4402
|
+
(document) => String(document.document_id || "") === primaryDocumentId
|
|
4403
|
+
) || materializedPrdDocuments[0];
|
|
4404
|
+
prdFile = primaryDocument?.local_file || null;
|
|
4405
|
+
if (prdFile) {
|
|
4406
|
+
contextSource = "prd_document_set";
|
|
4407
|
+
contextNote = "Readable PRD documents are stored in the linked document files.";
|
|
4408
|
+
} else {
|
|
4409
|
+
contextSource = "prd_declared_but_unavailable";
|
|
4410
|
+
contextNote = "A PRD document set exists, but readable document text was not included in this sync snapshot.";
|
|
4411
|
+
}
|
|
4412
|
+
} else if (prdText) {
|
|
4413
|
+
const prdFilename = `${sanitizeFileSegment(feedbackId, `feedback-${index + 1}`)}.md`;
|
|
4414
|
+
const prdPath = path.join(stagedPrdRoot, prdFilename);
|
|
4415
|
+
writeTextFile(prdPath, ensureTrailingNewline(prdText));
|
|
4416
|
+
prdFile = toPosixRelativePath(targetRoot, path.join(prdSyncDir, prdFilename));
|
|
3596
4417
|
contextSource = attachmentDocuments.length > 0 ? "attachment_file" : "prd_file";
|
|
3597
4418
|
contextNote = attachmentDocuments.length > 0
|
|
3598
4419
|
? "Readable feedback attachment documents are stored in the linked file."
|
|
@@ -3604,31 +4425,34 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
3604
4425
|
}
|
|
3605
4426
|
|
|
3606
4427
|
return {
|
|
3607
|
-
...item,
|
|
3608
|
-
feedback_id: feedbackId,
|
|
3609
|
-
conversation_turns: conversationTurns,
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
4428
|
+
...item,
|
|
4429
|
+
feedback_id: feedbackId,
|
|
4430
|
+
conversation_turns: conversationTurns,
|
|
4431
|
+
prd_documents: materializedPrdDocuments,
|
|
4432
|
+
context_source: contextSource,
|
|
4433
|
+
context_note: contextNote,
|
|
4434
|
+
prd_file: prdFile,
|
|
3613
4435
|
};
|
|
3614
4436
|
});
|
|
3615
4437
|
const snapshotCounts = snapshot.counts && typeof snapshot.counts === "object"
|
|
3616
4438
|
? { ...snapshot.counts }
|
|
3617
4439
|
: { total_feedback: materializedItems.length };
|
|
3618
|
-
snapshotCounts.with_prd_files = prdFileCount;
|
|
4440
|
+
snapshotCounts.with_prd_files = prdFileCount;
|
|
4441
|
+
snapshotCounts.with_prd_document_sets = prdDocumentSetCount;
|
|
3619
4442
|
if (snapshotCounts.with_conversation_turns === undefined) {
|
|
3620
4443
|
snapshotCounts.with_conversation_turns = materializedItems.filter((item) => Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0).length;
|
|
3621
4444
|
}
|
|
3622
4445
|
|
|
3623
|
-
const payload = scrubBootstrapValue({
|
|
4446
|
+
const payload = scrubBootstrapValue({
|
|
3624
4447
|
schema_version: snapshot.schema_version || 2,
|
|
3625
4448
|
project: snapshot.project || null,
|
|
3626
4449
|
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
3627
4450
|
filters: snapshot.filters && typeof snapshot.filters === "object" ? snapshot.filters : {},
|
|
3628
4451
|
counts: snapshotCounts,
|
|
3629
|
-
artifacts: {
|
|
3630
|
-
feedback_prd_root: "PRD/feedback-sync",
|
|
3631
|
-
with_prd_files: prdFileCount,
|
|
4452
|
+
artifacts: {
|
|
4453
|
+
feedback_prd_root: "PRD/feedback-sync",
|
|
4454
|
+
with_prd_files: prdFileCount,
|
|
4455
|
+
with_prd_document_sets: prdDocumentSetCount,
|
|
3632
4456
|
},
|
|
3633
4457
|
queue: materializedItems
|
|
3634
4458
|
.filter((item) => String(item?.status || "").trim().toLowerCase() !== "resolved")
|
|
@@ -3638,24 +4462,55 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
3638
4462
|
priority: item?.priority || null,
|
|
3639
4463
|
title: item?.title || null,
|
|
3640
4464
|
})),
|
|
3641
|
-
items: materializedItems.map((item) => {
|
|
3642
|
-
const nextItem = { ...item };
|
|
3643
|
-
delete nextItem.prd_text;
|
|
3644
|
-
|
|
3645
|
-
|
|
4465
|
+
items: materializedItems.map((item) => {
|
|
4466
|
+
const nextItem = { ...item };
|
|
4467
|
+
delete nextItem.prd_text;
|
|
4468
|
+
nextItem.prd_documents = (nextItem.prd_documents || []).map((rawDocument) => {
|
|
4469
|
+
const document = isPlainObject(rawDocument) ? { ...rawDocument } : {};
|
|
4470
|
+
delete document.source_markdown;
|
|
4471
|
+
delete document.canonical_markdown;
|
|
4472
|
+
return document;
|
|
4473
|
+
});
|
|
4474
|
+
return nextItem;
|
|
4475
|
+
}),
|
|
3646
4476
|
pagination: snapshot.pagination && typeof snapshot.pagination === "object" ? snapshot.pagination : undefined,
|
|
3647
4477
|
generated_at: snapshot.generated_at || null,
|
|
3648
|
-
snapshot_hash: snapshot.snapshot_hash || null,
|
|
3649
|
-
});
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
}
|
|
4478
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
4479
|
+
});
|
|
4480
|
+
try {
|
|
4481
|
+
const replacements = [];
|
|
4482
|
+
if (snapshot.project && typeof snapshot.project === "object") {
|
|
4483
|
+
const stagedProjectPath = path.join(stagedDataRoot, "project.yml");
|
|
4484
|
+
writeYamlFile(stagedProjectPath, scrubBootstrapValue(snapshot.project));
|
|
4485
|
+
replacements.push({
|
|
4486
|
+
staged: stagedProjectPath,
|
|
4487
|
+
target: path.join(dataRoot, "project.yml"),
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
const stagedFeedbackPath = path.join(stagedDataRoot, "feedback.yml");
|
|
4491
|
+
writeYamlFile(stagedFeedbackPath, payload);
|
|
4492
|
+
replacements.push(
|
|
4493
|
+
{
|
|
4494
|
+
staged: stagedPrdRoot,
|
|
4495
|
+
target: prdSyncDir,
|
|
4496
|
+
},
|
|
4497
|
+
{
|
|
4498
|
+
staged: stagedFeedbackPath,
|
|
4499
|
+
target: path.join(dataRoot, "feedback.yml"),
|
|
4500
|
+
}
|
|
4501
|
+
);
|
|
4502
|
+
replacePathsTransaction(replacements, transactionToken);
|
|
4503
|
+
pruneLegacyCommandCenterArtifacts(dataRoot, { bootstrap: true, feedback: true });
|
|
4504
|
+
return {
|
|
4505
|
+
targetRoot,
|
|
4506
|
+
dataRoot,
|
|
4507
|
+
prdSyncDir,
|
|
4508
|
+
manifest: payload,
|
|
4509
|
+
};
|
|
4510
|
+
} finally {
|
|
4511
|
+
if (fs.existsSync(stagingRoot)) removePathIfExists(stagingRoot);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
3659
4514
|
|
|
3660
4515
|
function resolveFeedbackReviewPaths(args, { requireFeedback = false } = {}) {
|
|
3661
4516
|
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
@@ -3731,9 +4586,14 @@ function feedbackChange(from, to) {
|
|
|
3731
4586
|
return { from: from === undefined ? null : from, to: to === undefined ? null : to };
|
|
3732
4587
|
}
|
|
3733
4588
|
|
|
3734
|
-
function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
|
|
3735
|
-
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
3736
|
-
const
|
|
4589
|
+
function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
|
|
4590
|
+
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
4591
|
+
const documentId = firstNonEmptyString(
|
|
4592
|
+
args["document-id"],
|
|
4593
|
+
args.documentId,
|
|
4594
|
+
args.document_id
|
|
4595
|
+
);
|
|
4596
|
+
const artifact = {
|
|
3737
4597
|
schema_version: 1,
|
|
3738
4598
|
kind: "feedback_refinement",
|
|
3739
4599
|
project_id: firstNonEmptyString(manifest?.project?.id, item?.project_id),
|
|
@@ -3741,9 +4601,10 @@ function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, c
|
|
|
3741
4601
|
title_snapshot: firstNonEmptyString(item?.title),
|
|
3742
4602
|
base_snapshot_hash: firstNonEmptyString(item?.snapshot_hash),
|
|
3743
4603
|
base_updated_at: firstNonEmptyString(item?.updated_at),
|
|
3744
|
-
review_action: action,
|
|
3745
|
-
reason: reason || null,
|
|
3746
|
-
|
|
4604
|
+
review_action: action,
|
|
4605
|
+
reason: reason || null,
|
|
4606
|
+
document_id: documentId || undefined,
|
|
4607
|
+
changes,
|
|
3747
4608
|
force: Boolean(args.force) || undefined,
|
|
3748
4609
|
client_session_id: clientSessionId || undefined,
|
|
3749
4610
|
created_at: new Date().toISOString(),
|
|
@@ -4447,7 +5308,7 @@ async function reviewProjectSuggestions({ apiBase, key, timeoutMs, payload, idem
|
|
|
4447
5308
|
});
|
|
4448
5309
|
}
|
|
4449
5310
|
|
|
4450
|
-
async function runCreatePrd(args) {
|
|
5311
|
+
async function runCreatePrd(args) {
|
|
4451
5312
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
4452
5313
|
if (!key) {
|
|
4453
5314
|
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
@@ -4469,13 +5330,24 @@ async function runCreatePrd(args) {
|
|
|
4469
5330
|
.flatMap((value) => toStringArray(value))
|
|
4470
5331
|
.map((item) => item.trim())
|
|
4471
5332
|
.filter(Boolean);
|
|
4472
|
-
const providedPaths = explicitPaths.length ? explicitPaths.concat(positionalPaths) : positionalPaths;
|
|
4473
|
-
const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && providedPaths.length === 0));
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
5333
|
+
const providedPaths = explicitPaths.length ? explicitPaths.concat(positionalPaths) : positionalPaths;
|
|
5334
|
+
const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && providedPaths.length === 0));
|
|
5335
|
+
const documentSetMode = Boolean(
|
|
5336
|
+
args["document-set"] || args.documentSet || args.document_set
|
|
5337
|
+
);
|
|
5338
|
+
|
|
5339
|
+
if (useStdin && providedPaths.length) {
|
|
5340
|
+
console.error("Use either file paths or --stdin for create-prd, not both.");
|
|
5341
|
+
process.exit(1);
|
|
5342
|
+
}
|
|
5343
|
+
if (documentSetMode && useStdin) {
|
|
5344
|
+
console.error("--document-set requires one to three markdown file paths; --stdin is not supported.");
|
|
5345
|
+
process.exit(1);
|
|
5346
|
+
}
|
|
5347
|
+
if (documentSetMode && (providedPaths.length < 1 || providedPaths.length > 3)) {
|
|
5348
|
+
console.error("--document-set requires between one and three markdown file paths.");
|
|
5349
|
+
process.exit(1);
|
|
5350
|
+
}
|
|
4479
5351
|
|
|
4480
5352
|
const buildCreatePrdPayload = ({ sourceText, filePath, includeClientRef = false }) => {
|
|
4481
5353
|
const trimmedSource = String(sourceText || "").trim();
|
|
@@ -4507,8 +5379,8 @@ async function runCreatePrd(args) {
|
|
|
4507
5379
|
return payload;
|
|
4508
5380
|
};
|
|
4509
5381
|
|
|
4510
|
-
let payloads = [];
|
|
4511
|
-
if (useStdin) {
|
|
5382
|
+
let payloads = [];
|
|
5383
|
+
if (useStdin) {
|
|
4512
5384
|
try {
|
|
4513
5385
|
payloads = [
|
|
4514
5386
|
buildCreatePrdPayload({
|
|
@@ -4527,21 +5399,72 @@ async function runCreatePrd(args) {
|
|
|
4527
5399
|
printHelp();
|
|
4528
5400
|
process.exit(1);
|
|
4529
5401
|
}
|
|
4530
|
-
try {
|
|
4531
|
-
|
|
4532
|
-
const absPath = path.resolve(providedPath);
|
|
4533
|
-
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
4534
|
-
throw new Error(`PRD file not found: ${absPath}`);
|
|
4535
|
-
}
|
|
4536
|
-
return
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
5402
|
+
try {
|
|
5403
|
+
const resolvedFiles = providedPaths.map((providedPath) => {
|
|
5404
|
+
const absPath = path.resolve(providedPath);
|
|
5405
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
5406
|
+
throw new Error(`PRD file not found: ${absPath}`);
|
|
5407
|
+
}
|
|
5408
|
+
return {
|
|
5409
|
+
absPath,
|
|
5410
|
+
sourceText: fs.readFileSync(absPath, "utf8"),
|
|
5411
|
+
};
|
|
5412
|
+
});
|
|
5413
|
+
if (documentSetMode) {
|
|
5414
|
+
const documents = resolvedFiles.map(({ absPath, sourceText }, index) => {
|
|
5415
|
+
const extension = path.extname(absPath).toLowerCase();
|
|
5416
|
+
if (![".md", ".markdown"].includes(extension)) {
|
|
5417
|
+
throw new Error(`PRD document-set files must use .md or .markdown: ${absPath}`);
|
|
5418
|
+
}
|
|
5419
|
+
if (!String(sourceText || "").trim()) {
|
|
5420
|
+
throw new Error(`PRD content is empty: ${absPath}`);
|
|
5421
|
+
}
|
|
5422
|
+
if (Array.from(sourceText).length > MAX_PRD_DOCUMENT_MARKDOWN_CHARS) {
|
|
5423
|
+
throw new Error(
|
|
5424
|
+
`PRD document-set files must contain at most ${MAX_PRD_DOCUMENT_MARKDOWN_CHARS} characters: ${absPath}`,
|
|
5425
|
+
);
|
|
5426
|
+
}
|
|
5427
|
+
const clientRef = path.relative(process.cwd(), absPath) || path.basename(absPath);
|
|
5428
|
+
return {
|
|
5429
|
+
client_ref: clientRef,
|
|
5430
|
+
filename: path.basename(absPath),
|
|
5431
|
+
title: String(
|
|
5432
|
+
extractMarkdownTitle(sourceText) || path.parse(absPath).name || `Document ${index + 1}`
|
|
5433
|
+
).trim(),
|
|
5434
|
+
source_markdown: sourceText,
|
|
5435
|
+
};
|
|
5436
|
+
});
|
|
5437
|
+
const parentTitle = String(args.title || documents[0]?.title || "").trim();
|
|
5438
|
+
if (!parentTitle) {
|
|
5439
|
+
throw new Error("A title is required for a PRD document set.");
|
|
5440
|
+
}
|
|
5441
|
+
const documentSetPayload = {
|
|
5442
|
+
title: parentTitle,
|
|
5443
|
+
documents,
|
|
5444
|
+
};
|
|
5445
|
+
const description = String(
|
|
5446
|
+
args.description || args["feedback-text"] || args.feedbackText || ""
|
|
5447
|
+
).trim();
|
|
5448
|
+
if (description) documentSetPayload.description = description;
|
|
5449
|
+
const priority = firstNonEmptyString(args.priority);
|
|
5450
|
+
const status = firstNonEmptyString(args.status);
|
|
5451
|
+
const tags = parseFeedbackTags(args);
|
|
5452
|
+
if (priority) documentSetPayload.priority = priority;
|
|
5453
|
+
if (status) documentSetPayload.status = status;
|
|
5454
|
+
if (tags) documentSetPayload.tags = tags;
|
|
5455
|
+
payloads = [documentSetPayload];
|
|
5456
|
+
} else {
|
|
5457
|
+
payloads = resolvedFiles.map(({ absPath, sourceText }) =>
|
|
5458
|
+
buildCreatePrdPayload({
|
|
5459
|
+
sourceText,
|
|
5460
|
+
filePath: absPath,
|
|
5461
|
+
includeClientRef: providedPaths.length > 1,
|
|
5462
|
+
})
|
|
5463
|
+
);
|
|
5464
|
+
}
|
|
5465
|
+
} catch (err) {
|
|
5466
|
+
console.error(err?.message || err);
|
|
5467
|
+
process.exit(1);
|
|
4545
5468
|
}
|
|
4546
5469
|
}
|
|
4547
5470
|
|
|
@@ -4558,7 +5481,10 @@ async function runCreatePrd(args) {
|
|
|
4558
5481
|
if (clientSessionId) payload.client_session_id = clientSessionId;
|
|
4559
5482
|
payload = withWriteApproval(args, payload, {
|
|
4560
5483
|
artifactKind: "markdown",
|
|
4561
|
-
targets:
|
|
5484
|
+
targets: documentSetMode
|
|
5485
|
+
? payload.documents.map((document) => document.client_ref)
|
|
5486
|
+
: [firstNonEmptyString(payload.client_ref, payload.title, "create-prd")],
|
|
5487
|
+
batchCount: documentSetMode ? 1 : null,
|
|
4562
5488
|
});
|
|
4563
5489
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
4564
5490
|
args,
|
|
@@ -4591,9 +5517,12 @@ async function runCreatePrd(args) {
|
|
|
4591
5517
|
feedback_id: data.feedback_id || null,
|
|
4592
5518
|
project_id: data.project_id || null,
|
|
4593
5519
|
title: data.title || null,
|
|
4594
|
-
status: data.status || null,
|
|
4595
|
-
deterministic: data.deterministic === true,
|
|
4596
|
-
|
|
5520
|
+
status: data.status || null,
|
|
5521
|
+
deterministic: data.deterministic === true,
|
|
5522
|
+
document_count: data.document_count || null,
|
|
5523
|
+
primary_document_id: data.primary_document_id || null,
|
|
5524
|
+
documents: Array.isArray(data.documents) ? data.documents : [],
|
|
5525
|
+
},
|
|
4597
5526
|
null,
|
|
4598
5527
|
2
|
|
4599
5528
|
)
|
|
@@ -4671,10 +5600,386 @@ async function runCreatePrd(args) {
|
|
|
4671
5600
|
if (message && status !== "created") line.push(`- ${message}`);
|
|
4672
5601
|
console.log(line.join(" "));
|
|
4673
5602
|
}
|
|
4674
|
-
if (aggregated.failed_count > 0) process.exitCode = 1;
|
|
4675
|
-
}
|
|
4676
|
-
|
|
4677
|
-
|
|
5603
|
+
if (aggregated.failed_count > 0) process.exitCode = 1;
|
|
5604
|
+
}
|
|
5605
|
+
|
|
5606
|
+
function readJsonFileQuiet(filePath) {
|
|
5607
|
+
try {
|
|
5608
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
5609
|
+
} catch {
|
|
5610
|
+
return null;
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
|
|
5614
|
+
function packageRuntimeInfo() {
|
|
5615
|
+
const corePackage = readJsonFileQuiet(path.join(__dirname, "package.json")) || {};
|
|
5616
|
+
let wrapperPackage = null;
|
|
5617
|
+
const localWrapperPath = path.resolve(__dirname, "..", "myte", "package.json");
|
|
5618
|
+
if (fs.existsSync(localWrapperPath)) {
|
|
5619
|
+
wrapperPackage = readJsonFileQuiet(localWrapperPath);
|
|
5620
|
+
}
|
|
5621
|
+
if (!wrapperPackage) {
|
|
5622
|
+
try {
|
|
5623
|
+
wrapperPackage = require("myte/package.json");
|
|
5624
|
+
} catch {
|
|
5625
|
+
wrapperPackage = null;
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
const expectedCoreVersion =
|
|
5629
|
+
wrapperPackage?.dependencies?.["@mytegroupinc/myte-core"] || null;
|
|
5630
|
+
const runtimeDependencies = Object.entries(corePackage.dependencies || {})
|
|
5631
|
+
.map(([name, version]) => ({
|
|
5632
|
+
name,
|
|
5633
|
+
version: String(version),
|
|
5634
|
+
}))
|
|
5635
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
5636
|
+
return {
|
|
5637
|
+
wrapper: {
|
|
5638
|
+
name: wrapperPackage?.name || "myte",
|
|
5639
|
+
version: wrapperPackage?.version || null,
|
|
5640
|
+
},
|
|
5641
|
+
core: {
|
|
5642
|
+
name: corePackage?.name || "@mytegroupinc/myte-core",
|
|
5643
|
+
version: corePackage?.version || null,
|
|
5644
|
+
},
|
|
5645
|
+
compatible:
|
|
5646
|
+
!wrapperPackage ||
|
|
5647
|
+
!expectedCoreVersion ||
|
|
5648
|
+
expectedCoreVersion === corePackage?.version,
|
|
5649
|
+
expected_core_version: expectedCoreVersion,
|
|
5650
|
+
runtime_dependencies: runtimeDependencies,
|
|
5651
|
+
install_graph: {
|
|
5652
|
+
package_count: (wrapperPackage ? 2 : 1) + runtimeDependencies.length,
|
|
5653
|
+
explanation: wrapperPackage
|
|
5654
|
+
? "public wrapper + CLI core + core runtime dependencies"
|
|
5655
|
+
: "CLI core + core runtime dependencies",
|
|
5656
|
+
},
|
|
5657
|
+
};
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
function projectKeySummary(key = getProjectApiKey()) {
|
|
5661
|
+
const normalized = String(key || "").trim();
|
|
5662
|
+
return {
|
|
5663
|
+
present: Boolean(normalized),
|
|
5664
|
+
source: process.env.MYTE_API_KEY
|
|
5665
|
+
? "MYTE_API_KEY"
|
|
5666
|
+
: process.env.MYTE_PROJECT_API_KEY
|
|
5667
|
+
? "MYTE_PROJECT_API_KEY"
|
|
5668
|
+
: null,
|
|
5669
|
+
fingerprint: normalized
|
|
5670
|
+
? createHash("sha256").update(normalized).digest("hex").slice(0, 12)
|
|
5671
|
+
: null,
|
|
5672
|
+
};
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
function probeDns(
|
|
5676
|
+
hostname,
|
|
5677
|
+
timeoutMs,
|
|
5678
|
+
{
|
|
5679
|
+
lookup = dns.promises.lookup.bind(dns.promises),
|
|
5680
|
+
setTimeoutFn = setTimeout,
|
|
5681
|
+
clearTimeoutFn = clearTimeout,
|
|
5682
|
+
} = {},
|
|
5683
|
+
) {
|
|
5684
|
+
if (net.isIP(hostname)) {
|
|
5685
|
+
return Promise.resolve({
|
|
5686
|
+
ok: true,
|
|
5687
|
+
hostname,
|
|
5688
|
+
addresses: [{ address: hostname, family: net.isIP(hostname) }],
|
|
5689
|
+
});
|
|
5690
|
+
}
|
|
5691
|
+
return new Promise((resolve) => {
|
|
5692
|
+
let settled = false;
|
|
5693
|
+
let timer;
|
|
5694
|
+
const finish = (payload) => {
|
|
5695
|
+
if (settled) return;
|
|
5696
|
+
settled = true;
|
|
5697
|
+
if (timer !== undefined) clearTimeoutFn(timer);
|
|
5698
|
+
resolve(payload);
|
|
5699
|
+
};
|
|
5700
|
+
timer = setTimeoutFn(() => {
|
|
5701
|
+
const error = new Error("DNS probe timed out.");
|
|
5702
|
+
error.code = "ETIMEDOUT";
|
|
5703
|
+
finish({
|
|
5704
|
+
ok: false,
|
|
5705
|
+
hostname,
|
|
5706
|
+
failure: publicRequestFailure(
|
|
5707
|
+
createNetworkRequestError(error, `https://${hostname}`, { timedOut: true }),
|
|
5708
|
+
),
|
|
5709
|
+
});
|
|
5710
|
+
}, timeoutMs);
|
|
5711
|
+
Promise.resolve()
|
|
5712
|
+
.then(() => lookup(hostname, { all: true }))
|
|
5713
|
+
.then(
|
|
5714
|
+
(addresses) => {
|
|
5715
|
+
finish({
|
|
5716
|
+
ok: true,
|
|
5717
|
+
hostname,
|
|
5718
|
+
addresses: addresses.map((entry) => ({
|
|
5719
|
+
address: entry.address,
|
|
5720
|
+
family: entry.family,
|
|
5721
|
+
})),
|
|
5722
|
+
});
|
|
5723
|
+
},
|
|
5724
|
+
(error) => {
|
|
5725
|
+
finish({
|
|
5726
|
+
ok: false,
|
|
5727
|
+
hostname,
|
|
5728
|
+
failure: publicRequestFailure(
|
|
5729
|
+
createNetworkRequestError(error, `https://${hostname}`),
|
|
5730
|
+
),
|
|
5731
|
+
});
|
|
5732
|
+
},
|
|
5733
|
+
);
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
5736
|
+
|
|
5737
|
+
function probeDirectTransport(apiBase, timeoutMs) {
|
|
5738
|
+
const parsed = new URL(apiBase);
|
|
5739
|
+
const secure = parsed.protocol === "https:";
|
|
5740
|
+
const port = Number(parsed.port || (secure ? 443 : 80));
|
|
5741
|
+
const startedAt = Date.now();
|
|
5742
|
+
return new Promise((resolve) => {
|
|
5743
|
+
let settled = false;
|
|
5744
|
+
const finish = (payload) => {
|
|
5745
|
+
if (settled) return;
|
|
5746
|
+
settled = true;
|
|
5747
|
+
clearTimeout(timer);
|
|
5748
|
+
try {
|
|
5749
|
+
socket.destroy();
|
|
5750
|
+
} catch {}
|
|
5751
|
+
resolve({
|
|
5752
|
+
...payload,
|
|
5753
|
+
protocol: secure ? "tls" : "tcp",
|
|
5754
|
+
host: parsed.hostname,
|
|
5755
|
+
port,
|
|
5756
|
+
duration_ms: Date.now() - startedAt,
|
|
5757
|
+
});
|
|
5758
|
+
};
|
|
5759
|
+
const options = {
|
|
5760
|
+
host: parsed.hostname,
|
|
5761
|
+
port,
|
|
5762
|
+
...(secure && !net.isIP(parsed.hostname) ? { servername: parsed.hostname } : {}),
|
|
5763
|
+
};
|
|
5764
|
+
const socket = secure ? tls.connect(options) : net.connect(options);
|
|
5765
|
+
const timer = setTimeout(() => {
|
|
5766
|
+
const error = new Error("Transport probe timed out.");
|
|
5767
|
+
error.code = "ETIMEDOUT";
|
|
5768
|
+
finish({
|
|
5769
|
+
ok: false,
|
|
5770
|
+
failure: publicRequestFailure(
|
|
5771
|
+
createNetworkRequestError(error, apiBase, { timedOut: true }),
|
|
5772
|
+
),
|
|
5773
|
+
});
|
|
5774
|
+
}, timeoutMs);
|
|
5775
|
+
socket.once(secure ? "secureConnect" : "connect", () => {
|
|
5776
|
+
finish({
|
|
5777
|
+
ok: true,
|
|
5778
|
+
authorized: secure ? socket.authorized === true : null,
|
|
5779
|
+
tls_protocol: secure ? socket.getProtocol?.() || null : null,
|
|
5780
|
+
});
|
|
5781
|
+
});
|
|
5782
|
+
socket.once("error", (error) => {
|
|
5783
|
+
finish({
|
|
5784
|
+
ok: false,
|
|
5785
|
+
failure: publicRequestFailure(createNetworkRequestError(error, apiBase)),
|
|
5786
|
+
});
|
|
5787
|
+
});
|
|
5788
|
+
});
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
async function runInfo(args, envPath) {
|
|
5792
|
+
const packages = packageRuntimeInfo();
|
|
5793
|
+
const payload = {
|
|
5794
|
+
command: "myte",
|
|
5795
|
+
packages,
|
|
5796
|
+
node_version: process.version,
|
|
5797
|
+
platform: `${process.platform}-${process.arch}`,
|
|
5798
|
+
api_base: resolveApiBase(args),
|
|
5799
|
+
project_key: projectKeySummary(),
|
|
5800
|
+
proxy: proxyEnvironmentSummary(),
|
|
5801
|
+
workspace: {
|
|
5802
|
+
cwd: process.cwd(),
|
|
5803
|
+
env_file: envPath,
|
|
5804
|
+
},
|
|
5805
|
+
};
|
|
5806
|
+
if (args.json) {
|
|
5807
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
5808
|
+
return;
|
|
5809
|
+
}
|
|
5810
|
+
console.log(`myte ${packages.wrapper.version || packages.core.version || "unknown"}`);
|
|
5811
|
+
console.log(`core: ${packages.core.version || "unknown"}`);
|
|
5812
|
+
console.log(`wrapper/core compatible: ${packages.compatible ? "yes" : "no"}`);
|
|
5813
|
+
console.log(
|
|
5814
|
+
`runtime dependencies: ${
|
|
5815
|
+
packages.runtime_dependencies.length
|
|
5816
|
+
? packages.runtime_dependencies
|
|
5817
|
+
.map((dependency) => `${dependency.name}@${dependency.version}`)
|
|
5818
|
+
.join(", ")
|
|
5819
|
+
: "none"
|
|
5820
|
+
}`,
|
|
5821
|
+
);
|
|
5822
|
+
console.log(`npm install graph: ${packages.install_graph.package_count} packages`);
|
|
5823
|
+
console.log(`node: ${payload.node_version}`);
|
|
5824
|
+
console.log(`api: ${payload.api_base}`);
|
|
5825
|
+
console.log(`project key: ${payload.project_key.present ? "present" : "missing"}`);
|
|
5826
|
+
console.log(`proxy: ${payload.proxy.configured ? "configured" : "direct"}`);
|
|
5827
|
+
console.log(`workspace: ${payload.workspace.cwd}`);
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5830
|
+
async function runVersion(args) {
|
|
5831
|
+
const packages = packageRuntimeInfo();
|
|
5832
|
+
const version = packages.wrapper.version || packages.core.version || "unknown";
|
|
5833
|
+
if (args.verbose || args.json) {
|
|
5834
|
+
const payload = {
|
|
5835
|
+
version,
|
|
5836
|
+
...packages,
|
|
5837
|
+
node_version: process.version,
|
|
5838
|
+
compatible: packages.compatible,
|
|
5839
|
+
};
|
|
5840
|
+
if (args.json) {
|
|
5841
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
5842
|
+
} else {
|
|
5843
|
+
console.log(`myte ${version}`);
|
|
5844
|
+
console.log(`core ${packages.core.version || "unknown"}`);
|
|
5845
|
+
console.log(
|
|
5846
|
+
`runtime dependencies: ${
|
|
5847
|
+
packages.runtime_dependencies.length
|
|
5848
|
+
? packages.runtime_dependencies
|
|
5849
|
+
.map((dependency) => `${dependency.name}@${dependency.version}`)
|
|
5850
|
+
.join(", ")
|
|
5851
|
+
: "none"
|
|
5852
|
+
}`,
|
|
5853
|
+
);
|
|
5854
|
+
console.log(`npm install graph: ${packages.install_graph.package_count} packages`);
|
|
5855
|
+
console.log(`node ${process.version}`);
|
|
5856
|
+
console.log(`wrapper/core compatible: ${packages.compatible ? "yes" : "no"}`);
|
|
5857
|
+
}
|
|
5858
|
+
return;
|
|
5859
|
+
}
|
|
5860
|
+
console.log(version);
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
async function runDoctor(args, envPath) {
|
|
5864
|
+
const timeoutMs = Math.max(
|
|
5865
|
+
1_000,
|
|
5866
|
+
Math.min(30_000, Number(args["timeout-ms"] || args.timeoutMs || 10_000) || 10_000),
|
|
5867
|
+
);
|
|
5868
|
+
const apiBase = resolveApiBase(args);
|
|
5869
|
+
const parsedBase = new URL(apiBase);
|
|
5870
|
+
const key = getProjectApiKey();
|
|
5871
|
+
const proxy = proxyEnvironmentSummary();
|
|
5872
|
+
const packages = packageRuntimeInfo();
|
|
5873
|
+
const payload = {
|
|
5874
|
+
schema_version: 1,
|
|
5875
|
+
command: "myte doctor",
|
|
5876
|
+
generated_at: new Date().toISOString(),
|
|
5877
|
+
packages,
|
|
5878
|
+
runtime: {
|
|
5879
|
+
node_version: process.version,
|
|
5880
|
+
platform: `${process.platform}-${process.arch}`,
|
|
5881
|
+
transport: null,
|
|
5882
|
+
},
|
|
5883
|
+
api: {
|
|
5884
|
+
base: apiBase,
|
|
5885
|
+
endpoint: parsedBase.host,
|
|
5886
|
+
},
|
|
5887
|
+
project_key: projectKeySummary(key),
|
|
5888
|
+
proxy,
|
|
5889
|
+
dns: null,
|
|
5890
|
+
direct_transport: null,
|
|
5891
|
+
config_probe: null,
|
|
5892
|
+
workspace: {
|
|
5893
|
+
cwd: process.cwd(),
|
|
5894
|
+
env_file: envPath,
|
|
5895
|
+
mode: "unknown",
|
|
5896
|
+
root: process.cwd(),
|
|
5897
|
+
configured_repos: [],
|
|
5898
|
+
found_repos: [],
|
|
5899
|
+
missing_repos: [],
|
|
5900
|
+
},
|
|
5901
|
+
ok: false,
|
|
5902
|
+
failure_layer: null,
|
|
5903
|
+
};
|
|
5904
|
+
|
|
5905
|
+
try {
|
|
5906
|
+
payload.runtime.transport = (await getFetchRuntime()).transport;
|
|
5907
|
+
} catch (error) {
|
|
5908
|
+
payload.runtime.transport = "unavailable";
|
|
5909
|
+
payload.config_probe = {
|
|
5910
|
+
ok: false,
|
|
5911
|
+
failure: publicRequestFailure(error),
|
|
5912
|
+
};
|
|
5913
|
+
}
|
|
5914
|
+
payload.dns = await probeDns(parsedBase.hostname, timeoutMs);
|
|
5915
|
+
payload.direct_transport = await probeDirectTransport(apiBase, timeoutMs);
|
|
5916
|
+
|
|
5917
|
+
if (!payload.config_probe && key) {
|
|
5918
|
+
try {
|
|
5919
|
+
const config = await fetchProjectConfig({ apiBase, key, timeoutMs });
|
|
5920
|
+
const repoNames = Array.isArray(config.repo_names) ? config.repo_names : [];
|
|
5921
|
+
const repoBindings = Array.isArray(config.repo_bindings) ? config.repo_bindings : [];
|
|
5922
|
+
const resolved = repoBindings.length
|
|
5923
|
+
? resolveConfiguredRepoBindings(repoBindings)
|
|
5924
|
+
: resolvePortableWorkspace(repoNames);
|
|
5925
|
+
payload.config_probe = {
|
|
5926
|
+
ok: true,
|
|
5927
|
+
project_id: config.project_id || null,
|
|
5928
|
+
repo_count: repoNames.length,
|
|
5929
|
+
};
|
|
5930
|
+
payload.workspace = {
|
|
5931
|
+
...payload.workspace,
|
|
5932
|
+
mode: resolved.mode || "unknown",
|
|
5933
|
+
root: resolved.root || process.cwd(),
|
|
5934
|
+
configured_repos: repoNames,
|
|
5935
|
+
found_repos: (resolved.repos || []).map((repo) => repo.name),
|
|
5936
|
+
missing_repos: resolved.missing || [],
|
|
5937
|
+
};
|
|
5938
|
+
} catch (error) {
|
|
5939
|
+
payload.config_probe = {
|
|
5940
|
+
ok: false,
|
|
5941
|
+
failure: publicRequestFailure(error),
|
|
5942
|
+
};
|
|
5943
|
+
}
|
|
5944
|
+
} else if (!payload.config_probe) {
|
|
5945
|
+
payload.config_probe = {
|
|
5946
|
+
ok: false,
|
|
5947
|
+
skipped: true,
|
|
5948
|
+
reason: "project_key_missing",
|
|
5949
|
+
};
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5952
|
+
payload.ok = payload.config_probe.ok === true;
|
|
5953
|
+
const firstFailure =
|
|
5954
|
+
payload.config_probe.failure ||
|
|
5955
|
+
(!payload.direct_transport.ok ? payload.direct_transport.failure : null) ||
|
|
5956
|
+
(!payload.dns.ok ? payload.dns.failure : null);
|
|
5957
|
+
payload.failure_layer = firstFailure?.layer || null;
|
|
5958
|
+
|
|
5959
|
+
if (args.json) {
|
|
5960
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
5961
|
+
} else {
|
|
5962
|
+
console.log(`Myte doctor: ${payload.ok ? "reachable" : "not ready"}`);
|
|
5963
|
+
console.log(`API: ${payload.api.base}`);
|
|
5964
|
+
console.log(`Key: ${payload.project_key.present ? "present" : "missing"}`);
|
|
5965
|
+
console.log(`Proxy: ${payload.proxy.configured ? "configured" : "direct"}`);
|
|
5966
|
+
console.log(`DNS: ${payload.dns.ok ? "ok" : "failed"}`);
|
|
5967
|
+
console.log(
|
|
5968
|
+
`Direct ${payload.direct_transport.protocol || "transport"}: ${
|
|
5969
|
+
payload.direct_transport.ok ? "ok" : "failed"
|
|
5970
|
+
}`,
|
|
5971
|
+
);
|
|
5972
|
+
console.log(`Authenticated config: ${payload.config_probe.ok ? "ok" : "failed"}`);
|
|
5973
|
+
if (firstFailure) {
|
|
5974
|
+
console.log(`Failure layer: ${firstFailure.layer || "unknown"}`);
|
|
5975
|
+
console.log(`Cause: ${firstFailure.code || "unknown"}`);
|
|
5976
|
+
console.log(`Detail: ${firstFailure.detail || "Request failed."}`);
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
if (!payload.ok) process.exitCode = 1;
|
|
5980
|
+
}
|
|
5981
|
+
|
|
5982
|
+
async function runConfig(args) {
|
|
4678
5983
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
4679
5984
|
if (!key) {
|
|
4680
5985
|
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
@@ -4821,10 +6126,11 @@ async function runBootstrap(args) {
|
|
|
4821
6126
|
return;
|
|
4822
6127
|
}
|
|
4823
6128
|
|
|
4824
|
-
const writeResult = writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir });
|
|
4825
|
-
summary.data_root = writeResult.dataRoot;
|
|
4826
|
-
|
|
4827
|
-
|
|
6129
|
+
const writeResult = writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir });
|
|
6130
|
+
summary.data_root = writeResult.dataRoot;
|
|
6131
|
+
summary.agents_myte_api_path = writeResult.agentsMyteApiPath;
|
|
6132
|
+
|
|
6133
|
+
if (includeSuggestions) {
|
|
4828
6134
|
let missionOpsSnapshot;
|
|
4829
6135
|
try {
|
|
4830
6136
|
missionOpsSnapshot = await fetchSuggestionsSyncSnapshot({ apiBase, key, timeoutMs, actorScope });
|
|
@@ -4855,9 +6161,10 @@ async function runBootstrap(args) {
|
|
|
4855
6161
|
console.log(`Output root: ${summary.output_root}`);
|
|
4856
6162
|
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
4857
6163
|
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
4858
|
-
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
4859
|
-
console.log(`Wrote: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
|
|
4860
|
-
if (summary.
|
|
6164
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
6165
|
+
console.log(`Wrote: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
|
|
6166
|
+
if (summary.agents_myte_api_path) console.log(`Wrote AgentsMyteAPI: ${summary.agents_myte_api_path}`);
|
|
6167
|
+
if (summary.mission_ops?.included) {
|
|
4861
6168
|
console.log(`Wrote mission ops: total_threads=${summary.mission_ops.total_threads}, actionable_threads=${summary.mission_ops.actionable_threads}`);
|
|
4862
6169
|
} else {
|
|
4863
6170
|
console.log("Skipped mission ops sync.");
|
|
@@ -4956,7 +6263,7 @@ async function runSyncQaqc(args) {
|
|
|
4956
6263
|
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
4957
6264
|
}
|
|
4958
6265
|
|
|
4959
|
-
async function runFeedbackSync(args) {
|
|
6266
|
+
async function runFeedbackSync(args) {
|
|
4960
6267
|
const key = getProjectApiKey();
|
|
4961
6268
|
if (!key) {
|
|
4962
6269
|
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
@@ -4965,17 +6272,45 @@ async function runFeedbackSync(args) {
|
|
|
4965
6272
|
|
|
4966
6273
|
const timeoutMs = resolveTimeoutMs(args);
|
|
4967
6274
|
const apiBase = resolveApiBase(args);
|
|
4968
|
-
const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
|
|
4969
|
-
|
|
6275
|
+
const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
|
|
6276
|
+
let pageSize;
|
|
6277
|
+
let maxItems;
|
|
6278
|
+
try {
|
|
6279
|
+
pageSize = resolveBoundedInteger(
|
|
6280
|
+
args["page-size"] || args.pageSize || args.page_size,
|
|
6281
|
+
{ fallback: DEFAULT_FEEDBACK_SYNC_PAGE_SIZE, min: 1, max: 500, label: "page-size" }
|
|
6282
|
+
);
|
|
6283
|
+
maxItems = resolveBoundedInteger(
|
|
6284
|
+
args.limit,
|
|
6285
|
+
{ fallback: 0, min: 0, max: 100_000, label: "limit" }
|
|
6286
|
+
);
|
|
6287
|
+
} catch (err) {
|
|
6288
|
+
console.error(err?.message || err);
|
|
6289
|
+
process.exit(1);
|
|
6290
|
+
}
|
|
6291
|
+
const filters = {
|
|
4970
6292
|
status: firstNonEmptyString(args.status) || "",
|
|
4971
6293
|
source: firstNonEmptyString(args.source) || "",
|
|
4972
6294
|
includePrdText,
|
|
4973
6295
|
includeCommentTurns: true,
|
|
4974
6296
|
};
|
|
4975
6297
|
|
|
4976
|
-
let snapshot;
|
|
4977
|
-
try {
|
|
4978
|
-
snapshot = await
|
|
6298
|
+
let snapshot;
|
|
6299
|
+
try {
|
|
6300
|
+
snapshot = await fetchCompleteFeedbackSyncSnapshot({
|
|
6301
|
+
apiBase,
|
|
6302
|
+
key,
|
|
6303
|
+
timeoutMs,
|
|
6304
|
+
filters,
|
|
6305
|
+
pageSize,
|
|
6306
|
+
maxItems,
|
|
6307
|
+
onPage: args.json
|
|
6308
|
+
? undefined
|
|
6309
|
+
: ({ page, received, synced, total }) => {
|
|
6310
|
+
const totalText = Number.isFinite(Number(total)) ? `/${total}` : "";
|
|
6311
|
+
console.error(`Feedback sync page ${page}: +${received} (${synced}${totalText})`);
|
|
6312
|
+
},
|
|
6313
|
+
});
|
|
4979
6314
|
} catch (err) {
|
|
4980
6315
|
console.error("Failed to fetch feedback sync snapshot:", err?.message || err);
|
|
4981
6316
|
process.exit(1);
|
|
@@ -5012,8 +6347,10 @@ async function runFeedbackSync(args) {
|
|
|
5012
6347
|
filters: snapshot.filters && typeof snapshot.filters === "object" ? snapshot.filters : filters,
|
|
5013
6348
|
counts,
|
|
5014
6349
|
snapshot_hash: snapshot.snapshot_hash || null,
|
|
5015
|
-
generated_at: snapshot.generated_at || null,
|
|
5016
|
-
|
|
6350
|
+
generated_at: snapshot.generated_at || null,
|
|
6351
|
+
pagination: snapshot.pagination || null,
|
|
6352
|
+
diagnostics: snapshot.diagnostics || null,
|
|
6353
|
+
dry_run: dryRun,
|
|
5017
6354
|
};
|
|
5018
6355
|
|
|
5019
6356
|
if (dryRun) {
|
|
@@ -5054,22 +6391,23 @@ async function runFeedbackSync(args) {
|
|
|
5054
6391
|
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
5055
6392
|
}
|
|
5056
6393
|
|
|
5057
|
-
async function refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeoutMs }) {
|
|
6394
|
+
async function refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeoutMs }) {
|
|
5058
6395
|
if (args.sync === false || args["no-sync"] || args.noSync || args.no_sync) {
|
|
5059
6396
|
return { skipped: true };
|
|
5060
6397
|
}
|
|
5061
6398
|
try {
|
|
5062
|
-
const snapshot = await
|
|
5063
|
-
apiBase,
|
|
5064
|
-
key,
|
|
5065
|
-
timeoutMs,
|
|
5066
|
-
filters: {
|
|
6399
|
+
const snapshot = await fetchCompleteFeedbackSyncSnapshot({
|
|
6400
|
+
apiBase,
|
|
6401
|
+
key,
|
|
6402
|
+
timeoutMs,
|
|
6403
|
+
filters: {
|
|
5067
6404
|
status: "",
|
|
5068
6405
|
source: "",
|
|
5069
6406
|
includePrdText: true,
|
|
5070
|
-
includeCommentTurns: true,
|
|
5071
|
-
},
|
|
5072
|
-
|
|
6407
|
+
includeCommentTurns: true,
|
|
6408
|
+
},
|
|
6409
|
+
pageSize: DEFAULT_FEEDBACK_SYNC_PAGE_SIZE,
|
|
6410
|
+
});
|
|
5073
6411
|
const resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
5074
6412
|
const writeResult = writeFeedbackSnapshot({
|
|
5075
6413
|
snapshot,
|
|
@@ -5152,9 +6490,62 @@ async function buildFeedbackDraftForCommand(args, subcommand) {
|
|
|
5152
6490
|
const tags = parseFeedbackTags(args);
|
|
5153
6491
|
if (tags) changes.tags = feedbackChange(Array.isArray(item.tags) ? item.tags : [], tags);
|
|
5154
6492
|
|
|
5155
|
-
const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
|
|
5156
|
-
if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
|
|
5157
|
-
|
|
6493
|
+
const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
|
|
6494
|
+
if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
|
|
6495
|
+
|
|
6496
|
+
const prdFile = firstNonEmptyString(args["prd-file"], args.prdFile, args.prd_file);
|
|
6497
|
+
if (prdFile) {
|
|
6498
|
+
const proposedPath = resolveInputFile(prdFile, "PRD document");
|
|
6499
|
+
const proposedMarkdown = fs.readFileSync(proposedPath, "utf8");
|
|
6500
|
+
if (!proposedMarkdown.trim()) {
|
|
6501
|
+
console.error("PRD document markdown cannot be empty.");
|
|
6502
|
+
process.exit(1);
|
|
6503
|
+
}
|
|
6504
|
+
if (proposedMarkdown.length > 250000) {
|
|
6505
|
+
console.error("PRD document markdown must be at most 250000 characters.");
|
|
6506
|
+
process.exit(1);
|
|
6507
|
+
}
|
|
6508
|
+
|
|
6509
|
+
const documents = Array.isArray(item.prd_documents) ? item.prd_documents : [];
|
|
6510
|
+
const explicitDocumentId = firstNonEmptyString(
|
|
6511
|
+
args["document-id"],
|
|
6512
|
+
args.documentId,
|
|
6513
|
+
args.document_id
|
|
6514
|
+
);
|
|
6515
|
+
const targetDocumentId = explicitDocumentId
|
|
6516
|
+
|| firstNonEmptyString(item.primary_prd_document_id)
|
|
6517
|
+
|| firstNonEmptyString(documents[0]?.document_id);
|
|
6518
|
+
const targetDocument = targetDocumentId
|
|
6519
|
+
? documents.find((document) => String(document?.document_id || "") === targetDocumentId)
|
|
6520
|
+
: null;
|
|
6521
|
+
if (explicitDocumentId && !targetDocument) {
|
|
6522
|
+
console.error(`PRD document not found in feedback.yml: ${explicitDocumentId}. Run \`myte feedback-sync\` first.`);
|
|
6523
|
+
process.exit(1);
|
|
6524
|
+
}
|
|
6525
|
+
if (documents.length > 0 && !targetDocument) {
|
|
6526
|
+
console.error("Unable to resolve the primary PRD document. Run `myte feedback-sync` first.");
|
|
6527
|
+
process.exit(1);
|
|
6528
|
+
}
|
|
6529
|
+
|
|
6530
|
+
let currentMarkdown = "";
|
|
6531
|
+
const localFile = firstNonEmptyString(targetDocument?.local_file, item.prd_file);
|
|
6532
|
+
if (localFile) {
|
|
6533
|
+
const currentPath = path.resolve(paths.targetRoot, localFile);
|
|
6534
|
+
const relativePath = path.relative(paths.targetRoot, currentPath);
|
|
6535
|
+
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath) && fs.existsSync(currentPath)) {
|
|
6536
|
+
currentMarkdown = fs.readFileSync(currentPath, "utf8");
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
changes.prd_markdown = feedbackChange(currentMarkdown, proposedMarkdown);
|
|
6540
|
+
changes.prd_format = feedbackChange(
|
|
6541
|
+
firstNonEmptyString(item.prd_format, "markdown"),
|
|
6542
|
+
"markdown"
|
|
6543
|
+
);
|
|
6544
|
+
if (targetDocumentId) {
|
|
6545
|
+
args["document-id"] = targetDocumentId;
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
} else {
|
|
5158
6549
|
console.error("Unknown feedback draft command. Use status, edit, assign, archive, or refine.");
|
|
5159
6550
|
process.exit(1);
|
|
5160
6551
|
}
|
|
@@ -5210,15 +6601,9 @@ async function runFeedbackValidateOrApply(args, mode) {
|
|
|
5210
6601
|
args.client_session_id,
|
|
5211
6602
|
payload.client_session_id
|
|
5212
6603
|
);
|
|
5213
|
-
if (mode === "apply") {
|
|
5214
|
-
payload = withWriteApproval(args, payload, {
|
|
5215
|
-
artifactKind: "yaml",
|
|
5216
|
-
targets: [feedbackId],
|
|
5217
|
-
});
|
|
5218
|
-
}
|
|
5219
6604
|
const idempotencyKey = mode === "apply"
|
|
5220
|
-
? resolveProjectMutationIdempotencyKey({
|
|
5221
|
-
args,
|
|
6605
|
+
? resolveProjectMutationIdempotencyKey({
|
|
6606
|
+
args,
|
|
5222
6607
|
operation: `feedback_refinement_apply:${feedbackId}`,
|
|
5223
6608
|
payload,
|
|
5224
6609
|
})
|
|
@@ -5515,7 +6900,7 @@ async function runFeedbackReviews(args) {
|
|
|
5515
6900
|
}
|
|
5516
6901
|
}
|
|
5517
6902
|
|
|
5518
|
-
function buildFeedbackReviewDecisionPayload(args, action) {
|
|
6903
|
+
function buildFeedbackReviewDecisionPayload(args, action) {
|
|
5519
6904
|
const filePath = firstNonEmptyString(args.file);
|
|
5520
6905
|
let payload = {};
|
|
5521
6906
|
if (filePath) {
|
|
@@ -5527,9 +6912,25 @@ function buildFeedbackReviewDecisionPayload(args, action) {
|
|
|
5527
6912
|
}
|
|
5528
6913
|
const nextPayload = { ...payload };
|
|
5529
6914
|
const reason = firstNonEmptyString(args.reason, args["review-note"], args.reviewNote, args.review_note);
|
|
5530
|
-
if (reason && !nextPayload.reason && !nextPayload.review_reason) {
|
|
5531
|
-
nextPayload.reason = reason;
|
|
5532
|
-
}
|
|
6915
|
+
if (reason && !nextPayload.reason && !nextPayload.review_reason) {
|
|
6916
|
+
nextPayload.reason = reason;
|
|
6917
|
+
}
|
|
6918
|
+
const verificationEvidence = firstNonEmptyString(
|
|
6919
|
+
args["verification-evidence"],
|
|
6920
|
+
args.verificationEvidence,
|
|
6921
|
+
args.verification_evidence,
|
|
6922
|
+
);
|
|
6923
|
+
if (verificationEvidence && !nextPayload.verification_evidence) {
|
|
6924
|
+
nextPayload.verification_evidence = verificationEvidence;
|
|
6925
|
+
}
|
|
6926
|
+
const deploymentEvidence = firstNonEmptyString(
|
|
6927
|
+
args["deployment-evidence"],
|
|
6928
|
+
args.deploymentEvidence,
|
|
6929
|
+
args.deployment_evidence,
|
|
6930
|
+
);
|
|
6931
|
+
if (deploymentEvidence && !nextPayload.deployment_evidence) {
|
|
6932
|
+
nextPayload.deployment_evidence = deploymentEvidence;
|
|
6933
|
+
}
|
|
5533
6934
|
const finalFile = firstNonEmptyString(args["final-file"], args.finalFile, args.final_file);
|
|
5534
6935
|
if (finalFile) {
|
|
5535
6936
|
nextPayload.final_change_set = readStructuredPayloadFile(finalFile, "Feedback final change set");
|
|
@@ -5569,20 +6970,27 @@ async function runFeedbackReviewDecision(args) {
|
|
|
5569
6970
|
console.error("Missing --request-ids for batch feedback review.");
|
|
5570
6971
|
process.exit(1);
|
|
5571
6972
|
}
|
|
5572
|
-
const reason = firstNonEmptyString(args.reason, args["review-note"], args.reviewNote, args.review_note);
|
|
6973
|
+
const reason = firstNonEmptyString(args.reason, args["review-note"], args.reviewNote, args.review_note);
|
|
6974
|
+
const verificationEvidence = firstNonEmptyString(
|
|
6975
|
+
args["verification-evidence"],
|
|
6976
|
+
args.verificationEvidence,
|
|
6977
|
+
args.verification_evidence,
|
|
6978
|
+
);
|
|
6979
|
+
const deploymentEvidence = firstNonEmptyString(
|
|
6980
|
+
args["deployment-evidence"],
|
|
6981
|
+
args.deploymentEvidence,
|
|
6982
|
+
args.deployment_evidence,
|
|
6983
|
+
);
|
|
5573
6984
|
let payload = {
|
|
5574
6985
|
...(isPlainObject(filePayload) && !Array.isArray(filePayload) ? filePayload : {}),
|
|
5575
6986
|
items,
|
|
5576
6987
|
action,
|
|
5577
6988
|
reason: reason || undefined,
|
|
6989
|
+
verification_evidence: verificationEvidence || undefined,
|
|
6990
|
+
deployment_evidence: deploymentEvidence || undefined,
|
|
5578
6991
|
};
|
|
5579
|
-
payload = withWriteApproval(args, payload, {
|
|
5580
|
-
artifactKind: "yaml",
|
|
5581
|
-
targets: items.map((item) => item.request_id || item.id).filter(Boolean),
|
|
5582
|
-
batchCount: items.length,
|
|
5583
|
-
});
|
|
5584
6992
|
const timeoutMs = resolveTimeoutMs(args);
|
|
5585
|
-
const apiBase = resolveApiBase(args);
|
|
6993
|
+
const apiBase = resolveApiBase(args);
|
|
5586
6994
|
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id, payload.client_session_id);
|
|
5587
6995
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
5588
6996
|
args,
|
|
@@ -5639,12 +7047,8 @@ async function runFeedbackReviewDecision(args) {
|
|
|
5639
7047
|
process.exit(1);
|
|
5640
7048
|
}
|
|
5641
7049
|
const endpointAction = action === "request_changes" ? "request-changes" : action === "cancel" ? "cancel" : "review";
|
|
5642
|
-
payload = withWriteApproval(args, payload, {
|
|
5643
|
-
artifactKind: "yaml",
|
|
5644
|
-
targets: [requestId],
|
|
5645
|
-
});
|
|
5646
7050
|
const timeoutMs = resolveTimeoutMs(args);
|
|
5647
|
-
const apiBase = resolveApiBase(args);
|
|
7051
|
+
const apiBase = resolveApiBase(args);
|
|
5648
7052
|
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id, payload.client_session_id);
|
|
5649
7053
|
const operationAction = action === "request_changes" ? "request_changes" : action;
|
|
5650
7054
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
@@ -5717,14 +7121,9 @@ async function runFeedbackMove(args) {
|
|
|
5717
7121
|
}
|
|
5718
7122
|
const batchId = firstNonEmptyString(args["batch-id"], args.batchId, args.batch_id);
|
|
5719
7123
|
if (batchId) payload.batch_id = batchId;
|
|
5720
|
-
payload = withWriteApproval(args, payload, {
|
|
5721
|
-
artifactKind: "yaml",
|
|
5722
|
-
targets: feedbackIds,
|
|
5723
|
-
batchCount: feedbackIds.length,
|
|
5724
|
-
});
|
|
5725
7124
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
5726
|
-
args,
|
|
5727
|
-
operation: `feedback_board_batch_move:${toState}:${feedbackIds.join(",")}`,
|
|
7125
|
+
args,
|
|
7126
|
+
operation: `feedback_board_batch_move:${toState}:${feedbackIds.join(",")}`,
|
|
5728
7127
|
payload,
|
|
5729
7128
|
});
|
|
5730
7129
|
let data;
|
|
@@ -5758,13 +7157,9 @@ async function runFeedbackMove(args) {
|
|
|
5758
7157
|
}
|
|
5759
7158
|
|
|
5760
7159
|
payload.from_state = fromState;
|
|
5761
|
-
payload = withWriteApproval(args, payload, {
|
|
5762
|
-
artifactKind: "yaml",
|
|
5763
|
-
targets: [feedbackId],
|
|
5764
|
-
});
|
|
5765
7160
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
5766
|
-
args,
|
|
5767
|
-
operation: `feedback_board_move:${feedbackId}`,
|
|
7161
|
+
args,
|
|
7162
|
+
operation: `feedback_board_move:${feedbackId}`,
|
|
5768
7163
|
payload,
|
|
5769
7164
|
});
|
|
5770
7165
|
|
|
@@ -5901,13 +7296,9 @@ async function runFeedbackUndo(args) {
|
|
|
5901
7296
|
const timeoutMs = resolveTimeoutMs(args);
|
|
5902
7297
|
const apiBase = resolveApiBase(args);
|
|
5903
7298
|
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
5904
|
-
payload = withWriteApproval(args, payload, {
|
|
5905
|
-
artifactKind: "yaml",
|
|
5906
|
-
targets: [feedbackId, eventId],
|
|
5907
|
-
});
|
|
5908
7299
|
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
5909
|
-
args,
|
|
5910
|
-
operation: `feedback_event_undo:${feedbackId}:${eventId}`,
|
|
7300
|
+
args,
|
|
7301
|
+
operation: `feedback_event_undo:${feedbackId}:${eventId}`,
|
|
5911
7302
|
payload,
|
|
5912
7303
|
});
|
|
5913
7304
|
|
|
@@ -5989,12 +7380,21 @@ async function runFeedbackPrdDiff(args) {
|
|
|
5989
7380
|
console.error("Missing --version-id.");
|
|
5990
7381
|
process.exit(1);
|
|
5991
7382
|
}
|
|
5992
|
-
const timeoutMs = resolveTimeoutMs(args);
|
|
5993
|
-
const apiBase = resolveApiBase(args);
|
|
5994
|
-
const compareTo = firstNonEmptyString(args["compare-to"], args.compareTo, args.compare_to);
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
7383
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
7384
|
+
const apiBase = resolveApiBase(args);
|
|
7385
|
+
const compareTo = firstNonEmptyString(args["compare-to"], args.compareTo, args.compare_to);
|
|
7386
|
+
const documentId = firstNonEmptyString(args["document-id"], args.documentId, args.document_id);
|
|
7387
|
+
let data;
|
|
7388
|
+
try {
|
|
7389
|
+
data = await fetchFeedbackPrdVersionDiff({
|
|
7390
|
+
apiBase,
|
|
7391
|
+
key,
|
|
7392
|
+
timeoutMs,
|
|
7393
|
+
feedbackId,
|
|
7394
|
+
versionId,
|
|
7395
|
+
compareTo,
|
|
7396
|
+
documentId,
|
|
7397
|
+
});
|
|
5998
7398
|
} catch (err) {
|
|
5999
7399
|
console.error("Feedback PRD diff failed:", err?.message || err);
|
|
6000
7400
|
process.exit(1);
|
|
@@ -6213,11 +7613,13 @@ async function buildSuggestionsMutationContext(args, mode) {
|
|
|
6213
7613
|
validateSuggestionsRevisePayload(payload);
|
|
6214
7614
|
}
|
|
6215
7615
|
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
7616
|
+
if (mode === "create" || mode === "revise") {
|
|
7617
|
+
payload = withWriteApproval(args, payload, {
|
|
7618
|
+
artifactKind: "yaml",
|
|
7619
|
+
targets: suggestionWriteTargets(items),
|
|
7620
|
+
batchCount: items.length > 1 ? items.length : null,
|
|
7621
|
+
});
|
|
7622
|
+
}
|
|
6221
7623
|
const idempotencyKey = resolveSuggestionsIdempotencyKey({
|
|
6222
7624
|
args,
|
|
6223
7625
|
mode,
|
|
@@ -6706,8 +8108,9 @@ async function runQuery(args) {
|
|
|
6706
8108
|
process.exit(1);
|
|
6707
8109
|
}
|
|
6708
8110
|
|
|
6709
|
-
const includeDiff = Boolean(args["with-diff"] || args.withDiff || args.diff || args.d);
|
|
6710
|
-
const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
|
|
8111
|
+
const includeDiff = Boolean(args["with-diff"] || args.withDiff || args.diff || args.d);
|
|
8112
|
+
const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
|
|
8113
|
+
const verbose = Boolean(args.verbose || args.v);
|
|
6711
8114
|
|
|
6712
8115
|
const fetchRemote = args.fetch !== undefined ? Boolean(args.fetch) : true;
|
|
6713
8116
|
|
|
@@ -6779,11 +8182,32 @@ async function runQuery(args) {
|
|
|
6779
8182
|
process.exit(0);
|
|
6780
8183
|
}
|
|
6781
8184
|
|
|
6782
|
-
let data;
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
if (
|
|
6786
|
-
|
|
8185
|
+
let data;
|
|
8186
|
+
const requestId = queryRequestId(args);
|
|
8187
|
+
try {
|
|
8188
|
+
if (verbose) {
|
|
8189
|
+
const runtime = await getFetchRuntime();
|
|
8190
|
+
console.error(
|
|
8191
|
+
`[myte] query request_id=${requestId} endpoint=${safeEndpointHost(apiBase)} transport=${runtime.transport}`,
|
|
8192
|
+
);
|
|
8193
|
+
}
|
|
8194
|
+
const queued = await withTransientQueryRetries(
|
|
8195
|
+
() =>
|
|
8196
|
+
createAssistantQueryJob({
|
|
8197
|
+
apiBase,
|
|
8198
|
+
key,
|
|
8199
|
+
payload,
|
|
8200
|
+
timeoutMs,
|
|
8201
|
+
requestId,
|
|
8202
|
+
}),
|
|
8203
|
+
{
|
|
8204
|
+
maxAttempts: 3,
|
|
8205
|
+
verbose,
|
|
8206
|
+
label: "query creation",
|
|
8207
|
+
},
|
|
8208
|
+
);
|
|
8209
|
+
if (queued.answer) {
|
|
8210
|
+
data = queued;
|
|
6787
8211
|
} else {
|
|
6788
8212
|
const jobId = firstNonEmptyString(queued.job_id, queued.id);
|
|
6789
8213
|
if (!jobId) {
|
|
@@ -6794,21 +8218,24 @@ async function runQuery(args) {
|
|
|
6794
8218
|
let finalStatus = null;
|
|
6795
8219
|
let pollDelayMs = 2_000;
|
|
6796
8220
|
do {
|
|
6797
|
-
await sleep(pollDelayMs);
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
8221
|
+
await sleep(pollDelayMs);
|
|
8222
|
+
finalStatus = await withTransientQueryRetries(
|
|
8223
|
+
() =>
|
|
8224
|
+
fetchAssistantQueryJobStatus({
|
|
8225
|
+
apiBase,
|
|
8226
|
+
key,
|
|
8227
|
+
timeoutMs,
|
|
8228
|
+
jobId,
|
|
8229
|
+
requestId,
|
|
8230
|
+
}),
|
|
8231
|
+
{
|
|
8232
|
+
maxAttempts: 4,
|
|
8233
|
+
verbose,
|
|
8234
|
+
label: "query status",
|
|
8235
|
+
initialDelayMs: Math.min(2_000, pollDelayMs),
|
|
8236
|
+
},
|
|
8237
|
+
);
|
|
8238
|
+
pollDelayMs = Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
|
|
6812
8239
|
if (["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
|
|
6813
8240
|
break;
|
|
6814
8241
|
}
|
|
@@ -6821,22 +8248,37 @@ async function runQuery(args) {
|
|
|
6821
8248
|
const detail = firstNonEmptyString(finalStatus?.error?.message, finalStatus?.error?.code);
|
|
6822
8249
|
throw new Error(detail || `Query job ${jobId} failed`);
|
|
6823
8250
|
}
|
|
6824
|
-
data = {
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
8251
|
+
data = {
|
|
8252
|
+
job_id: jobId,
|
|
8253
|
+
answer: finalStatus.answer,
|
|
8254
|
+
context_blocks: finalStatus.context_blocks,
|
|
8255
|
+
telemetry: finalStatus.telemetry,
|
|
6828
8256
|
};
|
|
6829
8257
|
}
|
|
6830
|
-
} catch (err) {
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
8258
|
+
} catch (err) {
|
|
8259
|
+
console.error("Assistant query failed:", err?.message || err);
|
|
8260
|
+
process.exit(1);
|
|
8261
|
+
}
|
|
8262
|
+
|
|
8263
|
+
if (args.json) {
|
|
8264
|
+
console.log(
|
|
8265
|
+
JSON.stringify(
|
|
8266
|
+
{
|
|
8267
|
+
ok: true,
|
|
8268
|
+
request_id: requestId,
|
|
8269
|
+
job_id: firstNonEmptyString(data.job_id, data.id) || null,
|
|
8270
|
+
answer: data.answer || "",
|
|
8271
|
+
context_blocks: Array.isArray(data.context_blocks) ? data.context_blocks : [],
|
|
8272
|
+
telemetry: data.telemetry || null,
|
|
8273
|
+
},
|
|
8274
|
+
null,
|
|
8275
|
+
2,
|
|
8276
|
+
),
|
|
8277
|
+
);
|
|
8278
|
+
return;
|
|
8279
|
+
}
|
|
8280
|
+
|
|
8281
|
+
console.log("Answer:\n", data.answer || "(no answer)");
|
|
6840
8282
|
if (data.context_blocks?.length) console.log(`\nContext blocks: ${data.context_blocks.length}`);
|
|
6841
8283
|
if (data.telemetry) {
|
|
6842
8284
|
const t = data.telemetry;
|
|
@@ -6850,8 +8292,8 @@ async function runChat(args) {
|
|
|
6850
8292
|
process.exit(1);
|
|
6851
8293
|
}
|
|
6852
8294
|
|
|
6853
|
-
async function main() {
|
|
6854
|
-
loadEnv();
|
|
8295
|
+
async function main() {
|
|
8296
|
+
const envPath = loadEnv();
|
|
6855
8297
|
|
|
6856
8298
|
const { command, rest } = splitCommand(process.argv.slice(2));
|
|
6857
8299
|
if (REMOVED_COMMAND_MESSAGES[command]) {
|
|
@@ -6863,12 +8305,27 @@ async function main() {
|
|
|
6863
8305
|
process.exit(1);
|
|
6864
8306
|
}
|
|
6865
8307
|
const args = parseArgs(rest);
|
|
6866
|
-
if (args.help || command === "help") {
|
|
8308
|
+
if (args.help || command === "help") {
|
|
6867
8309
|
printHelp();
|
|
6868
8310
|
return;
|
|
6869
|
-
}
|
|
6870
|
-
|
|
6871
|
-
if (command === "
|
|
8311
|
+
}
|
|
8312
|
+
|
|
8313
|
+
if (command === "version") {
|
|
8314
|
+
await runVersion(args);
|
|
8315
|
+
return;
|
|
8316
|
+
}
|
|
8317
|
+
|
|
8318
|
+
if (command === "info") {
|
|
8319
|
+
await runInfo(args, envPath);
|
|
8320
|
+
return;
|
|
8321
|
+
}
|
|
8322
|
+
|
|
8323
|
+
if (command === "doctor") {
|
|
8324
|
+
await runDoctor(args, envPath);
|
|
8325
|
+
return;
|
|
8326
|
+
}
|
|
8327
|
+
|
|
8328
|
+
if (command === "config") {
|
|
6872
8329
|
await runConfig(args);
|
|
6873
8330
|
return;
|
|
6874
8331
|
}
|
|
@@ -6948,12 +8405,19 @@ if (require.main === module) {
|
|
|
6948
8405
|
process.exit(1);
|
|
6949
8406
|
});
|
|
6950
8407
|
} else {
|
|
6951
|
-
module.exports = {
|
|
6952
|
-
|
|
6953
|
-
|
|
8408
|
+
module.exports = {
|
|
8409
|
+
buildFetchRuntime,
|
|
8410
|
+
buildMissionOpsPayload,
|
|
8411
|
+
classifyNetworkFailure,
|
|
8412
|
+
createHttpResponseError,
|
|
8413
|
+
createNetworkRequestError,
|
|
8414
|
+
collectCreateDraftPayload,
|
|
6954
8415
|
collectReviewPayload,
|
|
6955
8416
|
collectRevisionPayload,
|
|
6956
|
-
normalizeItemsPayload,
|
|
8417
|
+
normalizeItemsPayload,
|
|
8418
|
+
proxyEnvironmentSummary,
|
|
8419
|
+
probeDns,
|
|
8420
|
+
publicRequestFailure,
|
|
6957
8421
|
preserveMissionOpsWorkspace,
|
|
6958
8422
|
pruneLegacyCommandCenterArtifacts,
|
|
6959
8423
|
readYamlFile,
|
|
@@ -6965,6 +8429,8 @@ if (require.main === module) {
|
|
|
6965
8429
|
writeFeedbackSnapshot,
|
|
6966
8430
|
writeApprovedMissionCards,
|
|
6967
8431
|
writeMissionOpsSnapshot,
|
|
6968
|
-
writeQaqcSnapshot,
|
|
6969
|
-
|
|
6970
|
-
|
|
8432
|
+
writeQaqcSnapshot,
|
|
8433
|
+
fetchJsonWithTimeout,
|
|
8434
|
+
withTransientQueryRetries,
|
|
8435
|
+
};
|
|
8436
|
+
}
|