@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/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
- detail = "The authenticated project identity is not allowed to perform this action.";
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
- process.exit(1);
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
- console.error("Unknown feedback command. Use status, edit, document-add, assign, archive, submit, revise, reviews, review, move, comment, undo, prd-versions, prd-diff, get, history, validate, or apply.");
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,
@@ -11,6 +11,8 @@ const ARTIFACT_KINDS = new Set([
11
11
  "notification",
12
12
  "object",
13
13
  "query_job",
14
+ "feedback_ticket",
15
+ "ticket_batch",
14
16
  ]);
15
17
  const SENSITIVE_KEY = /(api[_-]?key|authorization|bearer|password|secret|token|credential)/i;
16
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.55",
3
+ "version": "0.0.56",
4
4
  "description": "Myte CLI core implementation.",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",