@mytegroupinc/myte-core 0.0.55 → 0.0.56
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 +36 -17
- package/cli.js +366 -63
- package/lib/certification-manifest.js +2 -0
- package/package.json +1 -1
- package/scripts/feedback-live-full-harness.js +145 -0
- package/scripts/mission-live-full-harness.js +10 -9
- package/scripts/project-assistant-live-full-harness.js +208 -174
package/cli.js
CHANGED
|
@@ -231,6 +231,12 @@ function printHelp() {
|
|
|
231
231
|
" myte feedback move --feedback-id <id> --to-state in_progress --from-state todo --reason \"...\" [--json]",
|
|
232
232
|
" myte feedback move --feedback-ids \"<id1,id2>\" --to-state in_progress --reason \"...\" [--json]",
|
|
233
233
|
" myte feedback comment --feedback-id <id> --body \"...\" [--body-file ./comment.md] --confirm-write --approval-artifact <path> [--json]",
|
|
234
|
+
" myte feedback tickets list --feedback-id <id> [--json]",
|
|
235
|
+
" myte feedback tickets create --feedback-id <id> --file ./ticket.yml --confirm-write --approval-artifact <path> [--json]",
|
|
236
|
+
" myte feedback tickets create --feedback-id <id> --file ./ticket-batch.yml --dry-run [--json]",
|
|
237
|
+
" myte feedback tickets update --feedback-id <id> --ticket-ids \"<id1,id2>\" --status in_progress --confirm-write --approval-artifact <path> [--json]",
|
|
238
|
+
" myte feedback tickets update --feedback-id <id> --file ./ticket-updates.yml --dry-run [--json]",
|
|
239
|
+
" myte feedback tickets coverage --feedback-id <id> --ticket-id <id> --version-id <id> --document-id <id> --coverage-status covered --confirm-write --approval-artifact <path> [--json]",
|
|
234
240
|
" myte feedback undo --feedback-id <id> --event-id <id> --reason \"...\" [--json]",
|
|
235
241
|
" myte feedback prd-versions --feedback-id <id> [--json]",
|
|
236
242
|
" myte feedback prd-diff --feedback-id <id> --version-id <id> [--compare-to <id>] [--document-id <id>] [--json]",
|
|
@@ -311,7 +317,8 @@ function printHelp() {
|
|
|
311
317
|
" - Explicit assignees must be the project owner or an active assigned project collaborator",
|
|
312
318
|
" - Never put the full PRD in description. The complete PRD must live in the markdown file body",
|
|
313
319
|
" - Each item carries the full PRD markdown blob as prd_markdown/ticket_markdown",
|
|
314
|
-
" - Backend stores that blob as the renderable PRD document source, mirrors it for PRD text search/sync, and generates a DOCX attachment",
|
|
320
|
+
" - Backend stores that blob as the renderable PRD document source, mirrors it for PRD text search/sync, and generates a DOCX attachment",
|
|
321
|
+
" - Successful creation returns active_prd_version_id, active_prd_version_number, and primary_document_id for immediate sub-ticket linking",
|
|
315
322
|
" - Renderer-friendly PRDs use one # title, ## sections, bullets, GitHub tables/checklists where useful, and code fences only for real code/config",
|
|
316
323
|
" - Do not wrap the whole PRD in a ```markdown fence; generic .md/.docx/.pdf/.txt uploads are normal attachments unless created as PRDs",
|
|
317
324
|
" - PRD DOCX content: the markdown body is stored verbatim",
|
|
@@ -320,10 +327,10 @@ function printHelp() {
|
|
|
320
327
|
" - Creates a project team comment through /api/project-assistant/project-comment",
|
|
321
328
|
" - Required: content (inline, --content, or --stdin)",
|
|
322
329
|
"",
|
|
323
|
-
"update-owner contract:",
|
|
324
|
-
" - Queues a durable owner email delivery through /api/project-assistant/update-owner",
|
|
330
|
+
"update-owner contract:",
|
|
331
|
+
" - Queues a durable owner email delivery through /api/project-assistant/update-owner",
|
|
325
332
|
" - Required: --subject and body markdown (via --body-markdown, --body-file, positional file/text, or --stdin)",
|
|
326
|
-
" - Resolves the owner synchronously; the existing notification worker delivers with a deterministic delivery id",
|
|
333
|
+
" - Resolves the owner synchronously; the existing notification worker delivers with a deterministic delivery id",
|
|
327
334
|
"",
|
|
328
335
|
"update-client contract:",
|
|
329
336
|
" - Creates a client update draft through /api/project-assistant/client-update-drafts",
|
|
@@ -352,6 +359,9 @@ function printHelp() {
|
|
|
352
359
|
" - move with --feedback-ids sends one batch board move request; it supports active-state moves and authorized archive",
|
|
353
360
|
" - comment creates text-only feedback-specific comments; attachments remain web UI only for this endpoint",
|
|
354
361
|
" - project-key API/CLI cannot unarchive archived Feedback; use the web archived Feedback view",
|
|
362
|
+
" - tickets list/create/update/coverage govern multiple engineering subtickets linked to the same PRD; create/update files accept JSON or YAML and batches are capped at 100",
|
|
363
|
+
" - Each ticket may link at most 25 PRD documents; the backend rejects version/document links outside that exact PRD before mutation",
|
|
364
|
+
" - tickets create/update --dry-run uses the server validation routes without mutating; live writes require revisions, idempotency and a structured approval artifact",
|
|
355
365
|
" - get/history read current feedback snapshots and audited event history through the project-key API",
|
|
356
366
|
" - prd-versions/prd-diff expose retained PRD versions and backend-generated per-document text diffs",
|
|
357
367
|
" - edit/refine with --prd-file updates a review artifact only; --document-id targets one document in a PRD document set",
|
|
@@ -361,7 +371,7 @@ function printHelp() {
|
|
|
361
371
|
" - apply is idempotent and does not rewrite local feedback.yml; run feedback-sync after apply to refresh local state",
|
|
362
372
|
"",
|
|
363
373
|
"approval artifact requirements:",
|
|
364
|
-
" - Required: create-prd, update-team, update-owner, update-client, feedback submit/revise/comment, suggestions create/revise",
|
|
374
|
+
" - Required: create-prd, update-team, update-owner, update-client, feedback submit/revise/comment, feedback tickets create/update/coverage, suggestions create/revise",
|
|
365
375
|
" - Not required: run-qaqc, mission status/archive, suggestions review, feedback review/move/undo/apply",
|
|
366
376
|
" - Never required: read/sync/query/validate commands, --print-context, --dry-run, and feedback local draft commands",
|
|
367
377
|
" - For required actions, prepare a .md/.markdown/.yml/.yaml/.json artifact, show it to the user, then rerun with --confirm-write --approval-artifact <path>",
|
|
@@ -383,6 +393,7 @@ function printHelp() {
|
|
|
383
393
|
" - move: direct audited board movement for allowed states; governed transitions remain backend-authorized",
|
|
384
394
|
" - document-add: create a governed proposal to append one linked PRD document to an existing Feedback item",
|
|
385
395
|
" - comment: create a text-only comment on one Feedback item",
|
|
396
|
+
" - tickets: list, validate, create and revision-fence single or batch PRD subticket lifecycle changes",
|
|
386
397
|
" - undo: reverse an audited board event when conflict checks allow it",
|
|
387
398
|
" - prd-versions: list retained PRD baselines/revisions for a feedback item",
|
|
388
399
|
" - prd-diff: fetch backend-generated text diff between PRD versions",
|
|
@@ -418,6 +429,10 @@ function printHelp() {
|
|
|
418
429
|
" --target-contact-ids Comma-separated client contact ObjectIds",
|
|
419
430
|
" --feedback-id <id> Feedback ObjectId for feedback review commands",
|
|
420
431
|
" --feedback-ids <ids> Comma-separated Feedback ObjectIds for batch feedback move",
|
|
432
|
+
" --ticket-id <id> Feedback PRD subticket ObjectId for single update or coverage",
|
|
433
|
+
" --ticket-ids <ids> Comma-separated subticket ObjectIds for one batch update",
|
|
434
|
+
" --severity <value> Ticket severity target: Critical, High, Medium, Low, or Info",
|
|
435
|
+
" --coverage-status <value> Ticket PRD coverage: not_covered, partially_covered, covered, or not_applicable",
|
|
421
436
|
" --request-id <id> Feedback review request ObjectId for submit/revise/review flows",
|
|
422
437
|
" --request-ids <ids> Comma-separated Feedback review request ObjectIds for batch review",
|
|
423
438
|
" --event-id <id> Feedback board event ObjectId for undo",
|
|
@@ -477,6 +492,9 @@ function printHelp() {
|
|
|
477
492
|
" myte feedback review --request-id 507f1f77bcf86cd799439022 --action approve --reason \"Looks correct\" --json",
|
|
478
493
|
" myte feedback move --feedback-id 507f1f77bcf86cd799439011 --to-state in_progress --reason \"Started\" --json",
|
|
479
494
|
" myte feedback comment --feedback-id 507f1f77bcf86cd799439011 --body-file ./comments/implementation.md --confirm-write --approval-artifact ./comments/implementation.md --json",
|
|
495
|
+
" myte feedback tickets list --feedback-id 507f1f77bcf86cd799439011 --json",
|
|
496
|
+
" myte feedback tickets create --feedback-id 507f1f77bcf86cd799439011 --file ./tickets/auth.yml --dry-run --json",
|
|
497
|
+
" myte feedback tickets update --feedback-id 507f1f77bcf86cd799439011 --ticket-ids \"507f1f77bcf86cd799439021,507f1f77bcf86cd799439022\" --status in_progress --confirm-write --approval-artifact ./tickets/update.yml --json",
|
|
480
498
|
" myte feedback document-add --feedback-id 507f1f77bcf86cd799439011 --prd-file ./drafts/sidebar-branding.md --title \"Individual Use Sidebar Branding\" --reason \"Link governed UX addendum to the Feedback context.\"",
|
|
481
499
|
" myte feedback validate --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
|
|
482
500
|
" myte feedback apply --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
|
|
@@ -2589,6 +2607,59 @@ async function postFeedbackComment({ apiBase, key, timeoutMs, feedbackId, payloa
|
|
|
2589
2607
|
return body.data || {};
|
|
2590
2608
|
}
|
|
2591
2609
|
|
|
2610
|
+
function parseFeedbackTicketIdsArg(args) {
|
|
2611
|
+
return dedupeStrings(
|
|
2612
|
+
parseCsvValues(
|
|
2613
|
+
args["ticket-ids"],
|
|
2614
|
+
args.ticketIds,
|
|
2615
|
+
args.ticket_ids,
|
|
2616
|
+
args["ticket-id"],
|
|
2617
|
+
args.ticketId,
|
|
2618
|
+
args.ticket_id
|
|
2619
|
+
)
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
async function requestFeedbackTickets({
|
|
2624
|
+
apiBase,
|
|
2625
|
+
key,
|
|
2626
|
+
timeoutMs,
|
|
2627
|
+
feedbackId,
|
|
2628
|
+
endpoint = "",
|
|
2629
|
+
method = "GET",
|
|
2630
|
+
payload,
|
|
2631
|
+
idempotencyKey,
|
|
2632
|
+
clientSessionId,
|
|
2633
|
+
}) {
|
|
2634
|
+
const fetchFn = await getFetch();
|
|
2635
|
+
const suffix = endpoint ? `/${String(endpoint).replace(/^\/+/, "")}` : "";
|
|
2636
|
+
const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/tickets${suffix}`;
|
|
2637
|
+
const headers = {
|
|
2638
|
+
Authorization: `Bearer ${key}`,
|
|
2639
|
+
...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
|
|
2640
|
+
};
|
|
2641
|
+
if (payload !== undefined) headers["Content-Type"] = "application/json";
|
|
2642
|
+
if (idempotencyKey) headers["X-Idempotency-Key"] = String(idempotencyKey).trim();
|
|
2643
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
2644
|
+
fetchFn,
|
|
2645
|
+
url,
|
|
2646
|
+
{
|
|
2647
|
+
method,
|
|
2648
|
+
headers,
|
|
2649
|
+
...(payload !== undefined ? { body: JSON.stringify(payload || {}) } : {}),
|
|
2650
|
+
},
|
|
2651
|
+
timeoutMs
|
|
2652
|
+
);
|
|
2653
|
+
if (!resp.ok || body.status !== "success") {
|
|
2654
|
+
const msg = body?.message || `Feedback ticket request failed (${resp.status})`;
|
|
2655
|
+
const err = new Error(msg);
|
|
2656
|
+
err.status = resp.status;
|
|
2657
|
+
err.data = body?.data;
|
|
2658
|
+
throw err;
|
|
2659
|
+
}
|
|
2660
|
+
return body.data || {};
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2592
2663
|
async function fetchFeedbackPrdVersions({ apiBase, key, timeoutMs, feedbackId }) {
|
|
2593
2664
|
const fetchFn = await getFetch();
|
|
2594
2665
|
const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/prd/versions`;
|
|
@@ -2807,7 +2878,7 @@ async function createMissionArchiveUpdate({ apiBase, key, timeoutMs, endpoint, p
|
|
|
2807
2878
|
return body.data || {};
|
|
2808
2879
|
}
|
|
2809
2880
|
|
|
2810
|
-
async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
|
|
2881
|
+
async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
|
|
2811
2882
|
const fetchFn = await getFetch();
|
|
2812
2883
|
const url = `${apiBase}/project-assistant/run-qaqc/${encodeURIComponent(String(batchId || ""))}`;
|
|
2813
2884
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
@@ -2832,15 +2903,15 @@ async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
|
|
|
2832
2903
|
if (retryAfter) err.retryAfter = retryAfter;
|
|
2833
2904
|
throw err;
|
|
2834
2905
|
}
|
|
2835
|
-
const data = body.data || {};
|
|
2836
|
-
Object.defineProperty(data, "_nextPollAfterMs", {
|
|
2837
|
-
value: successfulPollDelayMs(data, resp),
|
|
2838
|
-
enumerable: false,
|
|
2839
|
-
});
|
|
2840
|
-
return data;
|
|
2906
|
+
const data = body.data || {};
|
|
2907
|
+
Object.defineProperty(data, "_nextPollAfterMs", {
|
|
2908
|
+
value: successfulPollDelayMs(data, resp),
|
|
2909
|
+
enumerable: false,
|
|
2910
|
+
});
|
|
2911
|
+
return data;
|
|
2841
2912
|
}
|
|
2842
2913
|
|
|
2843
|
-
function resolveRetryAfterMs(err, fallbackMs = 5_000) {
|
|
2914
|
+
function resolveRetryAfterMs(err, fallbackMs = 5_000) {
|
|
2844
2915
|
const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
|
|
2845
2916
|
const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
|
|
2846
2917
|
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
|
|
@@ -2850,21 +2921,21 @@ function resolveRetryAfterMs(err, fallbackMs = 5_000) {
|
|
|
2850
2921
|
if (Number.isFinite(retryAfterDate)) {
|
|
2851
2922
|
return Math.min(Math.max(0, retryAfterDate - Date.now()), 60_000);
|
|
2852
2923
|
}
|
|
2853
|
-
return fallbackMs;
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
function successfulPollDelayMs(data, response, fallbackMs = 2_000) {
|
|
2857
|
-
const payloadDelay = Number(data?.next_poll_after_ms);
|
|
2858
|
-
const retryAfter = response?.headers?.get?.("retry-after");
|
|
2859
|
-
const headerDelay = retryAfter ? resolveRetryAfterMs({ retryAfter }, 0) : 0;
|
|
2860
|
-
const advertised = Math.max(
|
|
2861
|
-
Number.isFinite(payloadDelay) && payloadDelay > 0 ? payloadDelay : 0,
|
|
2862
|
-
headerDelay,
|
|
2863
|
-
);
|
|
2864
|
-
const base = Math.min(60_000, Math.max(250, advertised || fallbackMs));
|
|
2865
|
-
const jitterLimit = Math.max(0, Math.min(1_000, Number(process.env.MYTE_POLL_JITTER_MS ?? 250) || 0));
|
|
2866
|
-
return Math.min(60_000, base + (jitterLimit ? Math.floor(Math.random() * (jitterLimit + 1)) : 0));
|
|
2867
|
-
}
|
|
2924
|
+
return fallbackMs;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
function successfulPollDelayMs(data, response, fallbackMs = 2_000) {
|
|
2928
|
+
const payloadDelay = Number(data?.next_poll_after_ms);
|
|
2929
|
+
const retryAfter = response?.headers?.get?.("retry-after");
|
|
2930
|
+
const headerDelay = retryAfter ? resolveRetryAfterMs({ retryAfter }, 0) : 0;
|
|
2931
|
+
const advertised = Math.max(
|
|
2932
|
+
Number.isFinite(payloadDelay) && payloadDelay > 0 ? payloadDelay : 0,
|
|
2933
|
+
headerDelay,
|
|
2934
|
+
);
|
|
2935
|
+
const base = Math.min(60_000, Math.max(250, advertised || fallbackMs));
|
|
2936
|
+
const jitterLimit = Math.max(0, Math.min(1_000, Number(process.env.MYTE_POLL_JITTER_MS ?? 250) || 0));
|
|
2937
|
+
return Math.min(60_000, base + (jitterLimit ? Math.floor(Math.random() * (jitterLimit + 1)) : 0));
|
|
2938
|
+
}
|
|
2868
2939
|
|
|
2869
2940
|
function isTransientQueryStatusError(err) {
|
|
2870
2941
|
return Boolean(
|
|
@@ -2883,9 +2954,22 @@ function createHttpResponseError(resp, body, url, fallbackMessage) {
|
|
|
2883
2954
|
if (status === 401) {
|
|
2884
2955
|
kind = "authentication";
|
|
2885
2956
|
detail = "The API rejected the project key.";
|
|
2886
|
-
} else if (status === 403) {
|
|
2887
|
-
kind = "authorization";
|
|
2888
|
-
|
|
2957
|
+
} else if (status === 403) {
|
|
2958
|
+
kind = "authorization";
|
|
2959
|
+
const responseCode = firstNonEmptyString(body?.data?.code, body?.code);
|
|
2960
|
+
if (responseCode === "policy_consent_required") {
|
|
2961
|
+
const consentPath = firstNonEmptyString(body?.data?.consent_url, body?.data?.mission_board_deeplink);
|
|
2962
|
+
const requiredVersion = firstNonEmptyString(body?.data?.required_policy_version);
|
|
2963
|
+
detail = [
|
|
2964
|
+
firstNonEmptyString(body?.message) || "Myte Policy acceptance is required for this project.",
|
|
2965
|
+
requiredVersion ? `Required policy version: ${requiredVersion}.` : "",
|
|
2966
|
+
consentPath ? `Open the Myte mission board to review and accept: ${consentPath}` : "",
|
|
2967
|
+
]
|
|
2968
|
+
.filter(Boolean)
|
|
2969
|
+
.join(" ");
|
|
2970
|
+
} else {
|
|
2971
|
+
detail = "The authenticated project identity is not allowed to perform this action.";
|
|
2972
|
+
}
|
|
2889
2973
|
} else if (status === 429) {
|
|
2890
2974
|
kind = "rate_limit";
|
|
2891
2975
|
detail = "The API rate limit is temporarily exhausted.";
|
|
@@ -3008,7 +3092,7 @@ async function createAssistantQueryJob({
|
|
|
3008
3092
|
return body.data || {};
|
|
3009
3093
|
}
|
|
3010
3094
|
|
|
3011
|
-
async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId, requestId }) {
|
|
3095
|
+
async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId, requestId }) {
|
|
3012
3096
|
const fetchFn = await getFetch();
|
|
3013
3097
|
const url = `${apiBase}/project-assistant/query/${encodeURIComponent(String(jobId || ""))}`;
|
|
3014
3098
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
@@ -3027,12 +3111,12 @@ async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId, re
|
|
|
3027
3111
|
if (!resp.ok || body.status !== "success") {
|
|
3028
3112
|
throw createHttpResponseError(resp, body, url, "The query status could not be loaded.");
|
|
3029
3113
|
}
|
|
3030
|
-
const data = body.data || {};
|
|
3031
|
-
Object.defineProperty(data, "_nextPollAfterMs", {
|
|
3032
|
-
value: successfulPollDelayMs(data, resp),
|
|
3033
|
-
enumerable: false,
|
|
3034
|
-
});
|
|
3035
|
-
return data;
|
|
3114
|
+
const data = body.data || {};
|
|
3115
|
+
Object.defineProperty(data, "_nextPollAfterMs", {
|
|
3116
|
+
value: successfulPollDelayMs(data, resp),
|
|
3117
|
+
enumerable: false,
|
|
3118
|
+
});
|
|
3119
|
+
return data;
|
|
3036
3120
|
}
|
|
3037
3121
|
|
|
3038
3122
|
async function runRunQaqc(args) {
|
|
@@ -3092,15 +3176,15 @@ async function runRunQaqc(args) {
|
|
|
3092
3176
|
|
|
3093
3177
|
let finalStatus = null;
|
|
3094
3178
|
let syncSummary = null;
|
|
3095
|
-
if (shouldWait && data.batch_id) {
|
|
3096
|
-
const pollTimeoutMs = Math.max(timeoutMs, 900_000);
|
|
3097
|
-
const startedAt = Date.now();
|
|
3098
|
-
let pollDelayMs = 2_000;
|
|
3099
|
-
do {
|
|
3100
|
-
await sleep(pollDelayMs);
|
|
3101
|
-
try {
|
|
3102
|
-
finalStatus = await fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId: data.batch_id });
|
|
3103
|
-
pollDelayMs = Number(finalStatus?._nextPollAfterMs) || Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
|
|
3179
|
+
if (shouldWait && data.batch_id) {
|
|
3180
|
+
const pollTimeoutMs = Math.max(timeoutMs, 900_000);
|
|
3181
|
+
const startedAt = Date.now();
|
|
3182
|
+
let pollDelayMs = 2_000;
|
|
3183
|
+
do {
|
|
3184
|
+
await sleep(pollDelayMs);
|
|
3185
|
+
try {
|
|
3186
|
+
finalStatus = await fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId: data.batch_id });
|
|
3187
|
+
pollDelayMs = Number(finalStatus?._nextPollAfterMs) || Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
|
|
3104
3188
|
} catch (err) {
|
|
3105
3189
|
if (Number(err?.status) === 429) {
|
|
3106
3190
|
await sleep(resolveRetryAfterMs(err));
|
|
@@ -4221,6 +4305,7 @@ function buildAgentsMyteApiMarkdown() {
|
|
|
4221
4305
|
"- 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.",
|
|
4222
4306
|
"- 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.",
|
|
4223
4307
|
"- 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.",
|
|
4308
|
+
"- Feedback ticket create/update/coverage artifacts must enumerate every ticket key/id, expected revision, PRD document link, and changed lifecycle field. Use --dry-run before the approved live batch.",
|
|
4224
4309
|
"- 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.",
|
|
4225
4310
|
"- `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.",
|
|
4226
4311
|
"",
|
|
@@ -4265,6 +4350,10 @@ function buildAgentsMyteApiMarkdown() {
|
|
|
4265
4350
|
"| Feedback move | `myte feedback move --feedback-id <id> --to-state in_progress --reason \"...\" --json` | `POST /project-assistant/feedback/<id>/board-move` | governed write | no |",
|
|
4266
4351
|
"| 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 |",
|
|
4267
4352
|
"| 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 |",
|
|
4353
|
+
"| Feedback tickets list | `myte feedback tickets list --feedback-id <id> --json` | `GET /project-assistant/feedback/<id>/tickets` | read | no |",
|
|
4354
|
+
"| Feedback tickets create | `myte feedback tickets create --feedback-id <id> --file ./tickets.yml --confirm-write --approval-artifact ./tickets.yml --json` | `POST /project-assistant/feedback/<id>/tickets` or `/tickets/batch`; `--dry-run` uses `/tickets/batch/validate` | synchronous ticket write | required |",
|
|
4355
|
+
"| Feedback tickets update | `myte feedback tickets update --feedback-id <id> --ticket-ids \"<id1,id2>\" --status in_progress --confirm-write --approval-artifact ./update.yml --json` | `PATCH /project-assistant/feedback/<id>/tickets/batch`; `--dry-run` uses `/tickets/batch-update/validate` | revision-fenced ticket write | required |",
|
|
4356
|
+
"| Feedback ticket coverage | `myte feedback tickets coverage --feedback-id <id> --ticket-id <id> ... --confirm-write --approval-artifact ./coverage.yml --json` | `PATCH /project-assistant/feedback/<id>/tickets/<ticket_id>/prd-coverage` | revision-fenced ticket write | required |",
|
|
4268
4357
|
"| 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 |",
|
|
4269
4358
|
"| Feedback PRD versions | `myte feedback prd-versions --feedback-id <id> --json` | `GET /project-assistant/feedback/<id>/prd/versions` | read | no |",
|
|
4270
4359
|
"| 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 |",
|
|
@@ -5541,14 +5630,14 @@ async function runCreatePrd(args) {
|
|
|
5541
5630
|
console.error("--document-set requires one to three markdown file paths; --stdin is not supported.");
|
|
5542
5631
|
process.exit(1);
|
|
5543
5632
|
}
|
|
5544
|
-
if (documentSetMode && (providedPaths.length < 1 || providedPaths.length > 3)) {
|
|
5633
|
+
if (documentSetMode && (providedPaths.length < 1 || providedPaths.length > 3)) {
|
|
5545
5634
|
console.error("--document-set requires between one and three markdown file paths.");
|
|
5546
5635
|
process.exit(1);
|
|
5547
|
-
}
|
|
5548
|
-
if (!documentSetMode && providedPaths.length > 20) {
|
|
5549
|
-
console.error("create-prd accepts at most 20 files per batch.");
|
|
5550
|
-
process.exit(1);
|
|
5551
|
-
}
|
|
5636
|
+
}
|
|
5637
|
+
if (!documentSetMode && providedPaths.length > 20) {
|
|
5638
|
+
console.error("create-prd accepts at most 20 files per batch.");
|
|
5639
|
+
process.exit(1);
|
|
5640
|
+
}
|
|
5552
5641
|
|
|
5553
5642
|
const buildCreatePrdPayload = ({ sourceText, filePath, includeClientRef = false }) => {
|
|
5554
5643
|
const trimmedSource = String(sourceText || "").trim();
|
|
@@ -6195,12 +6284,15 @@ async function runConfig(args) {
|
|
|
6195
6284
|
const apiBase = normalizeApiBase(baseRaw);
|
|
6196
6285
|
|
|
6197
6286
|
let cfg;
|
|
6198
|
-
try {
|
|
6199
|
-
cfg = await fetchProjectConfig({ apiBase, key, timeoutMs });
|
|
6200
|
-
} catch (err) {
|
|
6201
|
-
console.error("Failed to fetch config:", err?.message || err);
|
|
6202
|
-
|
|
6203
|
-
|
|
6287
|
+
try {
|
|
6288
|
+
cfg = await fetchProjectConfig({ apiBase, key, timeoutMs });
|
|
6289
|
+
} catch (err) {
|
|
6290
|
+
console.error("Failed to fetch config:", err?.message || err);
|
|
6291
|
+
// Let Node drain its native fetch handles instead of forcing libuv closed.
|
|
6292
|
+
// Immediate process.exit() can abort on Windows after an HTTP error.
|
|
6293
|
+
process.exitCode = 1;
|
|
6294
|
+
return;
|
|
6295
|
+
}
|
|
6204
6296
|
|
|
6205
6297
|
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
6206
6298
|
const repoBindings = Array.isArray(cfg.repo_bindings) ? cfg.repo_bindings : [];
|
|
@@ -7710,6 +7802,213 @@ async function runFeedbackGetOrHistory(args, mode) {
|
|
|
7710
7802
|
console.log(`Snapshot: ${feedback.snapshot_hash || "n/a"}`);
|
|
7711
7803
|
}
|
|
7712
7804
|
|
|
7805
|
+
async function runFeedbackTickets(args) {
|
|
7806
|
+
const key = getProjectApiKey();
|
|
7807
|
+
if (!key) {
|
|
7808
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
7809
|
+
process.exit(1);
|
|
7810
|
+
}
|
|
7811
|
+
const action = String(firstNonEmptyString(args._?.[1], "list")).trim().toLowerCase().replace(/_/g, "-");
|
|
7812
|
+
const feedbackId = resolveFeedbackIdArg(args);
|
|
7813
|
+
if (!feedbackId) {
|
|
7814
|
+
console.error("Missing --feedback-id.");
|
|
7815
|
+
process.exit(1);
|
|
7816
|
+
}
|
|
7817
|
+
const apiBase = resolveApiBase(args);
|
|
7818
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
7819
|
+
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
7820
|
+
const dryRun = Boolean(args["dry-run"] || args.dryRun || action.startsWith("validate-"));
|
|
7821
|
+
|
|
7822
|
+
const call = async (requestOptions) => requestFeedbackTickets({
|
|
7823
|
+
apiBase,
|
|
7824
|
+
key,
|
|
7825
|
+
timeoutMs,
|
|
7826
|
+
feedbackId,
|
|
7827
|
+
clientSessionId,
|
|
7828
|
+
...requestOptions,
|
|
7829
|
+
});
|
|
7830
|
+
const print = (data) => {
|
|
7831
|
+
if (args.json) {
|
|
7832
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7833
|
+
return;
|
|
7834
|
+
}
|
|
7835
|
+
const tickets = Array.isArray(data?.tickets) ? data.tickets : [];
|
|
7836
|
+
if (tickets.length) {
|
|
7837
|
+
console.log(`Tickets: ${tickets.length}`);
|
|
7838
|
+
for (const ticket of tickets) {
|
|
7839
|
+
console.log(`- ${ticket.ticket_id || ticket._id || "(unknown)"} r${ticket.revision ?? "?"} ${ticket.status || "unknown"} ${ticket.severity || ""} ${ticket.title || ""}`.trim());
|
|
7840
|
+
}
|
|
7841
|
+
return;
|
|
7842
|
+
}
|
|
7843
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7844
|
+
};
|
|
7845
|
+
|
|
7846
|
+
try {
|
|
7847
|
+
if (action === "list") {
|
|
7848
|
+
print(await call({ method: "GET" }));
|
|
7849
|
+
return;
|
|
7850
|
+
}
|
|
7851
|
+
|
|
7852
|
+
const filePath = firstNonEmptyString(args.file);
|
|
7853
|
+
let rawPayload = filePath ? readStructuredPayloadFile(filePath, "Feedback ticket") : null;
|
|
7854
|
+
|
|
7855
|
+
if (["create", "validate-create"].includes(action)) {
|
|
7856
|
+
if (!rawPayload) {
|
|
7857
|
+
console.error("Missing --file with a JSON/YAML ticket payload.");
|
|
7858
|
+
process.exit(1);
|
|
7859
|
+
}
|
|
7860
|
+
const isBatch = Array.isArray(rawPayload) || (isPlainObject(rawPayload) && Array.isArray(rawPayload.items));
|
|
7861
|
+
let payload = isBatch ? normalizeItemsPayload(rawPayload, "Feedback ticket create") : { ...rawPayload };
|
|
7862
|
+
if (dryRun) {
|
|
7863
|
+
const validationPayload = isBatch ? payload : {
|
|
7864
|
+
items: [{
|
|
7865
|
+
...payload,
|
|
7866
|
+
ticket_key: firstNonEmptyString(payload.ticket_key, args["ticket-key"], `validate-${projectWritePayloadHash(payload).slice(0, 24)}`),
|
|
7867
|
+
}],
|
|
7868
|
+
};
|
|
7869
|
+
print(await call({ endpoint: "batch/validate", method: "POST", payload: validationPayload }));
|
|
7870
|
+
return;
|
|
7871
|
+
}
|
|
7872
|
+
const itemCount = isBatch ? payload.items.length : 1;
|
|
7873
|
+
const targets = isBatch
|
|
7874
|
+
? [feedbackId, ...payload.items.map((item) => firstNonEmptyString(item?.ticket_key)).filter(Boolean)]
|
|
7875
|
+
: [feedbackId];
|
|
7876
|
+
payload = withWriteApproval(args, payload, {
|
|
7877
|
+
artifactKind: "yaml",
|
|
7878
|
+
targets,
|
|
7879
|
+
batchCount: isBatch ? itemCount : null,
|
|
7880
|
+
});
|
|
7881
|
+
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
7882
|
+
args,
|
|
7883
|
+
operation: `feedback_ticket_${isBatch ? "batch_" : ""}create:${feedbackId}`,
|
|
7884
|
+
payload,
|
|
7885
|
+
});
|
|
7886
|
+
print(await call({
|
|
7887
|
+
endpoint: isBatch ? "batch" : "",
|
|
7888
|
+
method: "POST",
|
|
7889
|
+
payload,
|
|
7890
|
+
idempotencyKey,
|
|
7891
|
+
}));
|
|
7892
|
+
return;
|
|
7893
|
+
}
|
|
7894
|
+
|
|
7895
|
+
if (["update", "validate-update"].includes(action)) {
|
|
7896
|
+
const ticketIds = parseFeedbackTicketIdsArg(args);
|
|
7897
|
+
let payload;
|
|
7898
|
+
if (rawPayload) {
|
|
7899
|
+
payload = Array.isArray(rawPayload) ? { items: rawPayload } : { ...rawPayload };
|
|
7900
|
+
} else {
|
|
7901
|
+
if (!ticketIds.length) {
|
|
7902
|
+
console.error("Missing --file or --ticket-ids for ticket update.");
|
|
7903
|
+
process.exit(1);
|
|
7904
|
+
}
|
|
7905
|
+
const updates = {};
|
|
7906
|
+
for (const field of ["status", "severity"]) {
|
|
7907
|
+
const value = firstNonEmptyString(args[field]);
|
|
7908
|
+
if (value) updates[field] = value;
|
|
7909
|
+
}
|
|
7910
|
+
const assignedUserId = firstNonEmptyString(args["assigned-user-id"], args.assignedUserId, args.assigned_user_id);
|
|
7911
|
+
if (assignedUserId) updates.assigned_user_id = assignedUserId;
|
|
7912
|
+
if (!Object.keys(updates).length) {
|
|
7913
|
+
console.error("Missing ticket update fields. Use --status, --severity, --assigned-user-id, or --file.");
|
|
7914
|
+
process.exit(1);
|
|
7915
|
+
}
|
|
7916
|
+
const current = await call({ method: "GET" });
|
|
7917
|
+
const currentById = new Map((current.tickets || []).map((ticket) => [String(ticket.ticket_id || ticket._id), ticket]));
|
|
7918
|
+
const missing = ticketIds.filter((ticketId) => !currentById.has(String(ticketId)));
|
|
7919
|
+
if (missing.length) throw new Error(`Feedback ticket ids were not found: ${missing.join(", ")}`);
|
|
7920
|
+
payload = {
|
|
7921
|
+
items: ticketIds.map((ticketId) => ({
|
|
7922
|
+
ticket_id: ticketId,
|
|
7923
|
+
expected_revision: Number(currentById.get(String(ticketId)).revision),
|
|
7924
|
+
updates,
|
|
7925
|
+
})),
|
|
7926
|
+
};
|
|
7927
|
+
}
|
|
7928
|
+
const isBatch = Array.isArray(payload.items) || Array.isArray(payload.ticket_ids);
|
|
7929
|
+
if (dryRun) {
|
|
7930
|
+
const validationPayload = isBatch ? payload : {
|
|
7931
|
+
items: [{
|
|
7932
|
+
ticket_id: firstNonEmptyString(args["ticket-id"], args.ticketId, args.ticket_id, payload.ticket_id),
|
|
7933
|
+
expected_revision: payload.expected_revision,
|
|
7934
|
+
updates: payload.updates || Object.fromEntries(Object.entries(payload).filter(([key]) => ["title", "summary", "severity", "status", "assigned_user_id"].includes(key))),
|
|
7935
|
+
}],
|
|
7936
|
+
};
|
|
7937
|
+
print(await call({ endpoint: "batch-update/validate", method: "POST", payload: validationPayload }));
|
|
7938
|
+
return;
|
|
7939
|
+
}
|
|
7940
|
+
const itemCount = isBatch ? (payload.items || payload.ticket_ids || []).length : 1;
|
|
7941
|
+
const targetTicketId = firstNonEmptyString(args["ticket-id"], args.ticketId, args.ticket_id, payload.ticket_id);
|
|
7942
|
+
if (!isBatch && !targetTicketId) {
|
|
7943
|
+
console.error("Missing --ticket-id for a single ticket update.");
|
|
7944
|
+
process.exit(1);
|
|
7945
|
+
}
|
|
7946
|
+
const targetIds = isBatch
|
|
7947
|
+
? (payload.items || []).map((item) => item?.ticket_id).filter(Boolean).concat(payload.ticket_ids || [])
|
|
7948
|
+
: [targetTicketId];
|
|
7949
|
+
payload = withWriteApproval(args, payload, {
|
|
7950
|
+
artifactKind: "yaml",
|
|
7951
|
+
targets: [feedbackId, ...targetIds],
|
|
7952
|
+
batchCount: isBatch ? itemCount : null,
|
|
7953
|
+
});
|
|
7954
|
+
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
7955
|
+
args,
|
|
7956
|
+
operation: `feedback_ticket_${isBatch ? "batch_" : ""}update:${feedbackId}:${targetIds.join(",")}`,
|
|
7957
|
+
payload,
|
|
7958
|
+
});
|
|
7959
|
+
print(await call({
|
|
7960
|
+
endpoint: isBatch ? "batch" : encodeURIComponent(targetTicketId),
|
|
7961
|
+
method: "PATCH",
|
|
7962
|
+
payload,
|
|
7963
|
+
idempotencyKey,
|
|
7964
|
+
}));
|
|
7965
|
+
return;
|
|
7966
|
+
}
|
|
7967
|
+
|
|
7968
|
+
if (action === "coverage") {
|
|
7969
|
+
const ticketId = firstNonEmptyString(args["ticket-id"], args.ticketId, args.ticket_id);
|
|
7970
|
+
if (!ticketId) {
|
|
7971
|
+
console.error("Missing --ticket-id.");
|
|
7972
|
+
process.exit(1);
|
|
7973
|
+
}
|
|
7974
|
+
let payload = rawPayload && isPlainObject(rawPayload) ? { ...rawPayload } : {
|
|
7975
|
+
version_id: firstNonEmptyString(args["version-id"], args.versionId, args.version_id),
|
|
7976
|
+
document_id: firstNonEmptyString(args["document-id"], args.documentId, args.document_id),
|
|
7977
|
+
coverage_status: firstNonEmptyString(args["coverage-status"], args.coverageStatus, args.coverage_status),
|
|
7978
|
+
};
|
|
7979
|
+
if (payload.expected_revision === undefined) {
|
|
7980
|
+
const current = await call({ method: "GET" });
|
|
7981
|
+
const ticket = (current.tickets || []).find((item) => String(item.ticket_id || item._id) === String(ticketId));
|
|
7982
|
+
if (!ticket) throw new Error(`Feedback ticket id was not found: ${ticketId}`);
|
|
7983
|
+
payload.expected_revision = Number(ticket.revision);
|
|
7984
|
+
}
|
|
7985
|
+
if (dryRun) {
|
|
7986
|
+
console.log(JSON.stringify({ valid: true, validate_only: true, ticket_id: ticketId, payload }, null, 2));
|
|
7987
|
+
return;
|
|
7988
|
+
}
|
|
7989
|
+
payload = withWriteApproval(args, payload, { artifactKind: "yaml", targets: [feedbackId, ticketId] });
|
|
7990
|
+
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
7991
|
+
args,
|
|
7992
|
+
operation: `feedback_ticket_coverage:${feedbackId}:${ticketId}`,
|
|
7993
|
+
payload,
|
|
7994
|
+
});
|
|
7995
|
+
print(await call({ endpoint: `${encodeURIComponent(ticketId)}/prd-coverage`, method: "PATCH", payload, idempotencyKey }));
|
|
7996
|
+
return;
|
|
7997
|
+
}
|
|
7998
|
+
|
|
7999
|
+
console.error("Unknown feedback tickets command. Use list, create, validate-create, update, validate-update, or coverage.");
|
|
8000
|
+
process.exit(1);
|
|
8001
|
+
} catch (err) {
|
|
8002
|
+
if (args.json) {
|
|
8003
|
+
console.log(JSON.stringify({ ok: false, status: err?.status || null, message: err?.message || String(err), data: err?.data || null }, null, 2));
|
|
8004
|
+
} else {
|
|
8005
|
+
console.error("Feedback tickets failed:", err?.message || err);
|
|
8006
|
+
if (err?.data) console.error(JSON.stringify(err.data, null, 2));
|
|
8007
|
+
}
|
|
8008
|
+
process.exit(1);
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
|
|
7713
8012
|
async function runFeedback(args) {
|
|
7714
8013
|
const subcommand = firstNonEmptyString(args._?.[0]) || "help";
|
|
7715
8014
|
if (subcommand === "help") {
|
|
@@ -7764,7 +8063,11 @@ async function runFeedback(args) {
|
|
|
7764
8063
|
await runFeedbackGetOrHistory(args, subcommand);
|
|
7765
8064
|
return;
|
|
7766
8065
|
}
|
|
7767
|
-
|
|
8066
|
+
if (subcommand === "tickets") {
|
|
8067
|
+
await runFeedbackTickets(args);
|
|
8068
|
+
return;
|
|
8069
|
+
}
|
|
8070
|
+
console.error("Unknown feedback command. Use status, edit, document-add, assign, archive, submit, revise, reviews, review, move, comment, tickets, undo, prd-versions, prd-diff, get, history, validate, or apply.");
|
|
7768
8071
|
process.exit(1);
|
|
7769
8072
|
}
|
|
7770
8073
|
|
|
@@ -8495,7 +8798,7 @@ async function runQuery(args) {
|
|
|
8495
8798
|
initialDelayMs: Math.min(2_000, pollDelayMs),
|
|
8496
8799
|
},
|
|
8497
8800
|
);
|
|
8498
|
-
pollDelayMs = Number(finalStatus?._nextPollAfterMs) || Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
|
|
8801
|
+
pollDelayMs = Number(finalStatus?._nextPollAfterMs) || Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
|
|
8499
8802
|
if (["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
|
|
8500
8803
|
break;
|
|
8501
8804
|
}
|
|
@@ -8684,8 +8987,8 @@ if (require.main === module) {
|
|
|
8684
8987
|
resolveActorScope,
|
|
8685
8988
|
main,
|
|
8686
8989
|
scrubBootstrapValue,
|
|
8687
|
-
scrubMissionOpsValue,
|
|
8688
|
-
successfulPollDelayMs,
|
|
8990
|
+
scrubMissionOpsValue,
|
|
8991
|
+
successfulPollDelayMs,
|
|
8689
8992
|
writeBootstrapSnapshot,
|
|
8690
8993
|
writeFeedbackSnapshot,
|
|
8691
8994
|
writeApprovedMissionCards,
|