@mytegroupinc/myte-core 0.0.16 → 0.0.18

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 CHANGED
@@ -7,6 +7,7 @@ Most users should install the unscoped wrapper instead:
7
7
  - `npm install myte` then `npx myte ai "Return a JSON object with risks and next_steps" --json-response`
8
8
  - `npm install myte` then `npx myte bootstrap`
9
9
  - `npm install myte` then `npx myte run-qaqc --mission-ids M001 --wait --sync`
10
+ - `npm install myte` then `npx myte mission status --mission-ids M001 --status done`
10
11
  - `npm install myte` then `npx myte sync-qaqc`
11
12
  - `npm install myte` then `npx myte feedback-sync`
12
13
  - `npm install myte` then `npx myte suggestions sync`
@@ -20,6 +21,7 @@ Most users should install the unscoped wrapper instead:
20
21
  - `npm install myte` then `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
21
22
  - `npm i -g myte` then `myte bootstrap`
22
23
  - `npm i -g myte` then `myte run-qaqc --mission-ids M001 --wait --sync`
24
+ - `npm i -g myte` then `myte mission status --mission-ids M001 --status done`
23
25
  - `npm i -g myte` then `myte sync-qaqc`
24
26
  - `npm i -g myte` then `myte feedback-sync`
25
27
  - `npm i -g myte` then `myte suggestions sync`
@@ -32,6 +34,7 @@ Most users should install the unscoped wrapper instead:
32
34
  - `npm i -g myte` then `myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
33
35
  - `npx myte@latest bootstrap`
34
36
  - `npx myte@latest run-qaqc --mission-ids M001 --wait --sync`
37
+ - `npx myte@latest mission status --mission-ids M001 --status done`
35
38
  - `npx myte@latest sync-qaqc`
36
39
  - `npx myte@latest feedback-sync`
37
40
  - `npx myte@latest suggestions sync`
@@ -68,6 +71,9 @@ Notes:
68
71
  - On PowerShell, quote comma-separated multi-id values: `--mission-ids "M001,M002"`.
69
72
  - `run-qaqc --wait` polls `/api/project-assistant/run-qaqc/<batch_id>` until the batch is terminal.
70
73
  - `run-qaqc --sync` refreshes `MyteCommandCenter/data/qaqc.yml` after a completed batch.
74
+ - `mission status` updates one or many mission business ids through `/api/project-assistant/mission-status-update`.
75
+ - `mission status` normalizes status aliases like `todo`, `in_progress`, and `done`, then sends the canonical mission status values `Todo`, `In Progress`, or `Done`.
76
+ - `mission status` updates only the mission `status` field used by the app. It does not run QAQC and it does not rewrite `MyteCommandCenter/data/qaqc.yml`.
71
77
  - project-key QAQC runs through the dedicated `project_api_qaqc` queue inside the existing Celery service, with a global budget of `20` dispatch starts/minute and `20` live jobs.
72
78
  - a saturated `run-qaqc --wait` batch can take roughly `5-10` minutes before `--sync` has final data to write.
73
79
  - `sync-qaqc` works without `bootstrap`; it creates `MyteCommandCenter/data/qaqc.yml` automatically if missing.
@@ -76,6 +82,7 @@ Notes:
76
82
  - `sync-qaqc` keeps QAQC state in one deterministic file so the working set grows and shrinks with current active-mission reality.
77
83
  - `sync-qaqc` fully rewrites `MyteCommandCenter/data/qaqc.yml` on every sync and does not delete `MyteCommandCenter/data/missions/*.yml`.
78
84
  - `feedback-sync` writes one deterministic feedback snapshot under `MyteCommandCenter/data/feedback.yml`.
85
+ - `feedback-sync` defaults to all non-archived feedback unless `--status` is provided.
79
86
  - `feedback-sync` keeps feedback metadata plus comment turns in `MyteCommandCenter/data/feedback.yml`.
80
87
  - `feedback-sync` writes full PRD context into `MyteCommandCenter/PRD/feedback-sync/*.md` and points to those files from `feedback.yml`.
81
88
  - `feedback-sync` fully replaces the feedback-owned sync file to avoid stale local feedback noise.
@@ -122,6 +129,7 @@ Examples:
122
129
  - `npx myte ai "Return a JSON object with risks and next_steps" --json-response`
123
130
  - `npx myte bootstrap`
124
131
  - `npx myte run-qaqc --mission-ids "M001,M002" --wait --sync`
132
+ - `npx myte mission status --mission-ids "M001,M002" --status done`
125
133
  - `npx myte sync-qaqc`
126
134
  - `npx myte feedback-sync`
127
135
  - `npx myte suggestions sync`
@@ -139,3 +147,5 @@ Examples:
139
147
  - `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
140
148
 
141
149
  This package is published under the org scope for governance; the public `myte` wrapper delegates here.
150
+
151
+ `--json-response` sends both the Myte convenience flag (`myte_json_response: true`) and the OpenAI-compatible chat-completions shape (`response_format: { type: "json_object" }`). On the public developer API, that strict-JSON path also defaults to `medium` reasoning unless the caller explicitly overrides reasoning.
package/cli.js CHANGED
@@ -28,6 +28,7 @@ const REMOVED_COMMAND_MESSAGES = {
28
28
  "create-prds": "The `create-prds` alias has been removed. Use `myte create-prd <file.md> [more.md ...]`.",
29
29
  "add-prd": "The `add-prd` alias has been removed. Use `myte create-prd <file.md> [more.md ...]`.",
30
30
  prd: "The `prd` alias has been removed. Use `myte create-prd <file.md> [more.md ...]`.",
31
+ "qaqc-status-update": "The `qaqc-status-update` command has been removed. Use `myte mission status --mission-ids \"M001,M002\" --status todo|in_progress|done`.",
31
32
  };
32
33
 
33
34
  function findEnvPath(startDir) {
@@ -66,6 +67,16 @@ function loadEnv() {
66
67
  }
67
68
 
68
69
  function splitCommand(argv) {
70
+ if (argv[0] === "mission") {
71
+ if (argv[1] === "status" && argv[2] === "update") {
72
+ return { command: "mission-status", rest: argv.slice(3) };
73
+ }
74
+ if (argv[1] === "status") {
75
+ return { command: "mission-status", rest: argv.slice(2) };
76
+ }
77
+ return { command: "mission", rest: argv.slice(1) };
78
+ }
79
+
69
80
  const known = new Set([
70
81
  "query",
71
82
  "ai",
@@ -75,6 +86,8 @@ function splitCommand(argv) {
75
86
  "bootstrap",
76
87
  "suggestions",
77
88
  "run-qaqc",
89
+ "qaqc-status-update",
90
+ "mission",
78
91
  "sync-qaqc",
79
92
  "qaqc-sync",
80
93
  "create-prd",
@@ -189,6 +202,7 @@ function printHelp() {
189
202
  " myte config [--json]",
190
203
  " myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
191
204
  " myte run-qaqc --mission-ids \"M001[,M002...]\" [--wait] [--sync] [--force] [--json]",
205
+ " myte mission status --mission-ids \"M001[,M002...]\" --status todo|in_progress|done [--json]",
192
206
  " myte sync-qaqc [--output-dir ./MyteCommandCenter] [--json]",
193
207
  " myte suggestions sync [--output-dir ./MyteCommandCenter] [--json]",
194
208
  " myte suggestions create [--file ./payload.yml] [--no-sync] [--json]",
@@ -197,7 +211,7 @@ function printHelp() {
197
211
  " myte update-team \"<content>\" [--json]",
198
212
  " myte update-owner --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--json]",
199
213
  " myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] [--json]",
200
- " myte feedback-sync [--status Pending] [--source User] [--output-dir ./MyteCommandCenter] [--json]",
214
+ " myte feedback-sync [--status <value>] [--source <value>] [--with-prd-text|--no-with-prd-text] [--output-dir ./MyteCommandCenter] [--json]",
201
215
  " myte create-prd <file.md> [more.md ...] [--json] [--title \"...\"] [--description \"...\"]",
202
216
  " cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
203
217
  " cat update.md | myte update-owner --stdin --subject \"Owner update\"",
@@ -241,6 +255,12 @@ function printHelp() {
241
255
  " - Use --wait to poll batch status and --sync to refresh MyteCommandCenter/data/qaqc.yml after completion",
242
256
  " - On PowerShell, quote comma-separated mission ids: --mission-ids \"M001,M002\"",
243
257
  "",
258
+ "mission status contract:",
259
+ " - Updates the same mission `status` field used by the product board through /api/project-assistant/mission-status-update",
260
+ " - Accepts mission business ids and normalizes status aliases like todo, in_progress, and done",
261
+ " - Canonical mission statuses are exactly: Todo, In Progress, Done",
262
+ " - This only changes mission state. It does not queue QAQC and it does not sync MyteCommandCenter/data/qaqc.yml",
263
+ "",
244
264
  "create-prd contract:",
245
265
  " - Required: valid MYTE_API_KEY, PRD markdown body, title",
246
266
  " - Accepts one file or many files per command; multi-file uploads are sent in one deterministic batch request",
@@ -266,7 +286,8 @@ function printHelp() {
266
286
  "",
267
287
  "feedback-sync contract:",
268
288
  " - Runs from the wrapper root that contains the project's configured repo folders",
269
- " - Writes open project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
289
+ " - Syncs all non-archived feedback by default; use --status only when you want a narrower slice",
290
+ " - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
270
291
  " - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
271
292
  "",
272
293
  "Options:",
@@ -275,7 +296,7 @@ function printHelp() {
275
296
  " --timeout-ms <ms> Request timeout (default: 300000)",
276
297
  " --base-url <url> API base (default: https://api.myte.dev)",
277
298
  " --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
278
- " --json-response Ask the Myte AI gateway to return clean JSON only",
299
+ " --json-response Ask the Myte AI gateway to return clean JSON only and send OpenAI-compatible response_format",
279
300
  " --max-output-tokens Output token cap for `myte ai` simple queries",
280
301
  " --temperature <num> Temperature for `myte ai` simple queries",
281
302
  " --output-dir <path> Command Center output directory (default: <wrapper-root>/MyteCommandCenter)",
@@ -289,10 +310,11 @@ function printHelp() {
289
310
  " --body-file <path> Read update-owner or update-client markdown body from a file",
290
311
  " --target-contact-id Add one client contact ObjectId (repeatable)",
291
312
  " --target-contact-ids Comma-separated client contact ObjectIds",
292
- " --status <value> Feedback status filter for feedback-sync (default: Pending)",
313
+ " --status <value> For mission status: required target status (todo|in_progress|done). For feedback-sync: optional filter.",
293
314
  " --source <value> Feedback source filter for feedback-sync",
294
- " --with-prd-text Include extracted PRD text so local PRD files can be materialized during feedback-sync (default: on)",
295
- " --mission-ids <ids> Comma-separated mission business ids for run-qaqc (quote multi-id values on PowerShell)",
315
+ " --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
316
+ " --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
317
+ " --mission-ids <ids> Comma-separated mission business ids for run-qaqc or mission status (quote multi-id values on PowerShell)",
296
318
  " --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
297
319
  " --wait Poll batch status until terminal completion for run-qaqc",
298
320
  " --sync After run-qaqc completes, refresh local QAQC file",
@@ -311,12 +333,13 @@ function printHelp() {
311
333
  " myte suggestions revise --no-sync",
312
334
  " myte suggestions review --file ./review.yml",
313
335
  " myte run-qaqc --mission-ids \"M001,M002\" --wait --sync",
336
+ " myte mission status --mission-ids \"M001,M002\" --status done",
314
337
  " myte bootstrap --output-dir ./MyteCommandCenter",
315
338
  " myte sync-qaqc",
316
339
  " myte update-team \"Backend deploy completed; QAQC rerun queued.\"",
317
340
  " myte update-owner --subject \"QAQC progress\" --body-file ./updates/owner.md",
318
341
  " myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md",
319
- " myte feedback-sync --status Pending --source User",
342
+ " myte feedback-sync --json",
320
343
  " myte suggestions create --file ./suggestions/create.yml",
321
344
  " myte suggestions revise",
322
345
  " myte suggestions review",
@@ -397,6 +420,16 @@ function dedupeStrings(values) {
397
420
  return unique;
398
421
  }
399
422
 
423
+ function normalizeMissionStatusInput(value) {
424
+ const raw = String(value || "").trim();
425
+ if (!raw) return "";
426
+ const compact = raw.replace(/[\s_-]+/g, "").toLowerCase();
427
+ if (compact === "todo" || compact === "pending") return "Todo";
428
+ if (compact === "inprogress" || compact === "doing") return "In Progress";
429
+ if (compact === "done" || compact === "complete" || compact === "completed") return "Done";
430
+ return "";
431
+ }
432
+
400
433
  function resolveTimeoutMs(args) {
401
434
  const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
402
435
  const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
@@ -1067,9 +1100,15 @@ async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
1067
1100
  );
1068
1101
 
1069
1102
  if (!resp.ok || body.status !== "success") {
1103
+ const retryAfter = resp.headers?.get?.("retry-after");
1070
1104
  const msg = body?.message || `QAQC sync request failed (${resp.status})`;
1071
- const err = new Error(msg);
1105
+ const err = new Error(
1106
+ retryAfter
1107
+ ? `${msg} Retry after ${retryAfter}s.`
1108
+ : msg
1109
+ );
1072
1110
  err.status = resp.status;
1111
+ if (retryAfter) err.retryAfter = retryAfter;
1073
1112
  throw err;
1074
1113
  }
1075
1114
  return body.data || {};
@@ -1213,6 +1252,34 @@ async function createRunQaqcBatch({ apiBase, key, timeoutMs, payload, idempotenc
1213
1252
  return body.data || {};
1214
1253
  }
1215
1254
 
1255
+ async function createMissionStatusUpdate({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId }) {
1256
+ const fetchFn = await getFetch();
1257
+ const url = `${apiBase}/project-assistant/mission-status-update`;
1258
+ const { resp, body } = await fetchJsonWithTimeout(
1259
+ fetchFn,
1260
+ url,
1261
+ {
1262
+ method: "POST",
1263
+ headers: {
1264
+ "Content-Type": "application/json",
1265
+ Authorization: `Bearer ${key}`,
1266
+ "X-Idempotency-Key": String(idempotencyKey || "").trim(),
1267
+ ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
1268
+ },
1269
+ body: JSON.stringify(payload),
1270
+ },
1271
+ timeoutMs
1272
+ );
1273
+
1274
+ if (!resp.ok || body.status !== "success") {
1275
+ const msg = body?.message || `Mission status update request failed (${resp.status})`;
1276
+ const err = new Error(msg);
1277
+ err.status = resp.status;
1278
+ throw err;
1279
+ }
1280
+ return body.data || {};
1281
+ }
1282
+
1216
1283
  async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
1217
1284
  const fetchFn = await getFetch();
1218
1285
  const url = `${apiBase}/project-assistant/run-qaqc/${encodeURIComponent(String(batchId || ""))}`;
@@ -1227,15 +1294,30 @@ async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
1227
1294
  );
1228
1295
 
1229
1296
  if (!resp.ok || body.status !== "success") {
1297
+ const retryAfter = resp.headers?.get?.("retry-after");
1230
1298
  const msg = body?.message || `Run QAQC status request failed (${resp.status})`;
1231
- const err = new Error(msg);
1299
+ const err = new Error(
1300
+ retryAfter
1301
+ ? `${msg} Retry after ${retryAfter}s.`
1302
+ : msg
1303
+ );
1232
1304
  err.status = resp.status;
1305
+ if (retryAfter) err.retryAfter = retryAfter;
1233
1306
  throw err;
1234
1307
  }
1235
1308
  return body.data || {};
1236
1309
  }
1237
1310
 
1238
- async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
1311
+ function resolveRetryAfterMs(err, fallbackMs = 5_000) {
1312
+ const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
1313
+ const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
1314
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
1315
+ return Math.min(retryAfterSeconds * 1_000, 60_000);
1316
+ }
1317
+ return fallbackMs;
1318
+ }
1319
+
1320
+ async function createAssistantQueryJob({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
1239
1321
  const fetchFn = await getFetch();
1240
1322
  const url = `${apiBase}${endpoint}`;
1241
1323
  const { resp, body } = await fetchJsonWithTimeout(
@@ -1253,9 +1335,35 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint =
1253
1335
  );
1254
1336
 
1255
1337
  if (!resp.ok || body.status !== "success") {
1338
+ const retryAfter = resp.headers?.get?.("retry-after");
1256
1339
  const msg = body?.message || `Query failed (${resp.status})`;
1257
- const err = new Error(msg);
1340
+ const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
1341
+ err.status = resp.status;
1342
+ if (retryAfter) err.retryAfter = retryAfter;
1343
+ throw err;
1344
+ }
1345
+ return body.data || {};
1346
+ }
1347
+
1348
+ async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId }) {
1349
+ const fetchFn = await getFetch();
1350
+ const url = `${apiBase}/project-assistant/query/${encodeURIComponent(String(jobId || ""))}`;
1351
+ const { resp, body } = await fetchJsonWithTimeout(
1352
+ fetchFn,
1353
+ url,
1354
+ {
1355
+ method: "GET",
1356
+ headers: { Authorization: `Bearer ${key}` },
1357
+ },
1358
+ timeoutMs
1359
+ );
1360
+
1361
+ if (!resp.ok || body.status !== "success") {
1362
+ const retryAfter = resp.headers?.get?.("retry-after");
1363
+ const msg = body?.message || `Query status request failed (${resp.status})`;
1364
+ const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
1258
1365
  err.status = resp.status;
1366
+ if (retryAfter) err.retryAfter = retryAfter;
1259
1367
  throw err;
1260
1368
  }
1261
1369
  return body.data || {};
@@ -1326,6 +1434,10 @@ async function runRunQaqc(args) {
1326
1434
  try {
1327
1435
  finalStatus = await fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId: data.batch_id });
1328
1436
  } catch (err) {
1437
+ if (Number(err?.status) === 429) {
1438
+ await sleep(resolveRetryAfterMs(err));
1439
+ continue;
1440
+ }
1329
1441
  console.error("Failed to fetch QAQC batch status:", err?.message || err);
1330
1442
  process.exit(1);
1331
1443
  }
@@ -1341,11 +1453,18 @@ async function runRunQaqc(args) {
1341
1453
 
1342
1454
  if (shouldSync && ["completed", "completed_with_errors"].includes(String(finalStatus.status || "").trim())) {
1343
1455
  let snapshot;
1344
- try {
1345
- snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
1346
- } catch (err) {
1347
- console.error("Failed to fetch QAQC sync snapshot after batch completion:", err?.message || err);
1348
- process.exit(1);
1456
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1457
+ try {
1458
+ snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
1459
+ break;
1460
+ } catch (err) {
1461
+ if (Number(err?.status) === 429 && attempt < 4) {
1462
+ await sleep(resolveRetryAfterMs(err));
1463
+ continue;
1464
+ }
1465
+ console.error("Failed to fetch QAQC sync snapshot after batch completion:", err?.message || err);
1466
+ process.exit(1);
1467
+ }
1349
1468
  }
1350
1469
 
1351
1470
  let resolved;
@@ -1398,6 +1517,99 @@ async function runRunQaqc(args) {
1398
1517
  }
1399
1518
  }
1400
1519
 
1520
+ async function runMissionStatus(args) {
1521
+ const key = getProjectApiKey();
1522
+ if (!key) {
1523
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
1524
+ process.exit(1);
1525
+ }
1526
+
1527
+ const missionIds = parseMissionIdsArg(args);
1528
+ if (!missionIds.length) {
1529
+ console.error("Missing --mission-ids for `myte mission status`.");
1530
+ printHelp();
1531
+ process.exit(1);
1532
+ }
1533
+
1534
+ const newStatus = normalizeMissionStatusInput(firstNonEmptyString(args.status));
1535
+ if (!newStatus) {
1536
+ console.error("Missing or invalid --status for `myte mission status`. Use todo, in_progress, or done.");
1537
+ printHelp();
1538
+ process.exit(1);
1539
+ }
1540
+
1541
+ const payload = {
1542
+ mission_ids: missionIds,
1543
+ new_status: newStatus,
1544
+ };
1545
+ const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
1546
+ if (clientSessionId) payload.client_session_id = clientSessionId;
1547
+ const idempotencyKey = resolveProjectMutationIdempotencyKey({
1548
+ args,
1549
+ operation: "mission-status-update",
1550
+ payload,
1551
+ });
1552
+
1553
+ if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
1554
+ console.log(JSON.stringify(payload, null, 2));
1555
+ return;
1556
+ }
1557
+
1558
+ const timeoutMs = resolveTimeoutMs(args);
1559
+ const apiBase = resolveApiBase(args);
1560
+
1561
+ let data;
1562
+ try {
1563
+ data = await createMissionStatusUpdate({
1564
+ apiBase,
1565
+ key,
1566
+ timeoutMs,
1567
+ payload,
1568
+ idempotencyKey,
1569
+ clientSessionId,
1570
+ });
1571
+ } catch (err) {
1572
+ if (err?.name === "AbortError") {
1573
+ console.error(`Request timed out after ${timeoutMs}ms`);
1574
+ } else {
1575
+ console.error("Mission status update failed:", err?.message || err);
1576
+ }
1577
+ process.exit(1);
1578
+ }
1579
+
1580
+ const output = {
1581
+ project_id: data.project_id || null,
1582
+ requested_count: data.requested_count || missionIds.length,
1583
+ matched_count: data.matched_count || 0,
1584
+ updated_count: data.updated_count || 0,
1585
+ unchanged_count: data.unchanged_count || 0,
1586
+ rejected_count: data.rejected_count || 0,
1587
+ new_status: data.new_status || newStatus,
1588
+ missions: Array.isArray(data.missions) ? data.missions : [],
1589
+ };
1590
+
1591
+ if (args.json) {
1592
+ console.log(JSON.stringify(output, null, 2));
1593
+ return;
1594
+ }
1595
+
1596
+ if (output.project_id) console.log(`Project ID: ${output.project_id}`);
1597
+ console.log(`Target Status: ${output.new_status}`);
1598
+ console.log(`Requested: ${output.requested_count}`);
1599
+ console.log(`Matched: ${output.matched_count}`);
1600
+ console.log(`Updated: ${output.updated_count}`);
1601
+ console.log(`Unchanged: ${output.unchanged_count}`);
1602
+ console.log(`Rejected: ${output.rejected_count}`);
1603
+ for (const mission of output.missions) {
1604
+ const missionId = String(mission?.mission_id || "").trim() || "(unknown)";
1605
+ const status = String(mission?.status || "").trim() || "unknown";
1606
+ const detailParts = [];
1607
+ if (mission?.current_status) detailParts.push(`current=${mission.current_status}`);
1608
+ if (mission?.new_status) detailParts.push(`target=${mission.new_status}`);
1609
+ console.log(`- ${missionId}: ${status}${detailParts.length ? ` (${detailParts.join(", ")})` : ""}`);
1610
+ }
1611
+ }
1612
+
1401
1613
  function formatTargetContacts(contacts) {
1402
1614
  const items = Array.isArray(contacts) ? contacts : [];
1403
1615
  const formatted = items
@@ -1668,6 +1880,20 @@ function stableItemId(item, keys, fallback) {
1668
1880
  return fallback;
1669
1881
  }
1670
1882
 
1883
+ function bootstrapPathId(item, preferredKeys, fallbackKeys, fallback) {
1884
+ return stableItemId(item, preferredKeys, stableItemId(item, fallbackKeys, fallback));
1885
+ }
1886
+
1887
+ function bootstrapScopedPathId(item, preferredKeys, scopeKeys, fallbackKeys, fallback) {
1888
+ const preferred = stableItemId(item, preferredKeys, "");
1889
+ if (preferred) return preferred;
1890
+ const fallbackId = stableItemId(item, fallbackKeys, fallback);
1891
+ const scopeParts = (Array.isArray(scopeKeys) ? scopeKeys : [])
1892
+ .map((key) => String(item?.[key] || "").trim())
1893
+ .filter(Boolean);
1894
+ return [...scopeParts, fallbackId].filter(Boolean).join("__") || fallbackId;
1895
+ }
1896
+
1671
1897
  function stringifyYaml(value) {
1672
1898
  // eslint-disable-next-line global-require
1673
1899
  const YAML = require("yaml");
@@ -1889,20 +2115,42 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
1889
2115
  const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
1890
2116
 
1891
2117
  phases.forEach((phase, index) => {
1892
- const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
1893
- writeYamlFile(path.join(phasesDir, `${phaseId}.yml`), phase);
2118
+ const phasePathId = bootstrapPathId(
2119
+ phase,
2120
+ ["phase_key", "id"],
2121
+ ["phase_id"],
2122
+ `phase-${String(index + 1).padStart(3, "0")}`
2123
+ );
2124
+ writeYamlFile(path.join(phasesDir, `${phasePathId}.yml`), phase);
1894
2125
  });
1895
2126
  epics.forEach((epic, index) => {
1896
- const epicId = stableItemId(epic, ["epic_id", "id"], `E${String(index + 1).padStart(3, "0")}`);
1897
- writeYamlFile(path.join(epicsDir, `${epicId}.yml`), epic);
2127
+ const epicPathId = bootstrapScopedPathId(
2128
+ epic,
2129
+ ["epic_key", "id"],
2130
+ ["phase_key", "phase_id"],
2131
+ ["epic_id"],
2132
+ `epic-${String(index + 1).padStart(3, "0")}`
2133
+ );
2134
+ writeYamlFile(path.join(epicsDir, `${epicPathId}.yml`), epic);
1898
2135
  });
1899
2136
  stories.forEach((story, index) => {
1900
- const storyId = stableItemId(story, ["story_id", "id"], `S${String(index + 1).padStart(3, "0")}`);
1901
- writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
2137
+ const storyPathId = bootstrapScopedPathId(
2138
+ story,
2139
+ ["story_key", "id"],
2140
+ ["phase_key", "phase_id", "epic_key", "epic_id"],
2141
+ ["story_id"],
2142
+ `story-${String(index + 1).padStart(3, "0")}`
2143
+ );
2144
+ writeYamlFile(path.join(storiesDir, `${storyPathId}.yml`), story);
1902
2145
  });
1903
2146
  missions.forEach((mission, index) => {
1904
- const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
1905
- writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
2147
+ const missionPathId = bootstrapPathId(
2148
+ mission,
2149
+ ["mission_key", "id"],
2150
+ ["mission_id"],
2151
+ `mission-${String(index + 1).padStart(3, "0")}`
2152
+ );
2153
+ writeYamlFile(path.join(missionsDir, `${missionPathId}.yml`), mission);
1906
2154
  });
1907
2155
 
1908
2156
  if (snapshot.project && typeof snapshot.project === "object") {
@@ -2204,6 +2452,24 @@ function shouldSyncSuggestionsAfterMutation(args) {
2204
2452
  return !rawArgs.includes("--no-sync");
2205
2453
  }
2206
2454
 
2455
+ function resolveBooleanFlag(args, dashedName, defaultValue) {
2456
+ const rawArgs = Array.isArray(args?.__raw) ? args.__raw : [];
2457
+ const enabledToken = `--${dashedName}`;
2458
+ const disabledToken = `--no-${dashedName}`;
2459
+
2460
+ for (const token of rawArgs) {
2461
+ if (token === disabledToken) return false;
2462
+ if (token === enabledToken) return true;
2463
+ if (token.startsWith(`${enabledToken}=`)) {
2464
+ const rawValue = token.slice(enabledToken.length + 1).trim().toLowerCase();
2465
+ if (["1", "true", "yes", "on"].includes(rawValue)) return true;
2466
+ if (["0", "false", "no", "off"].includes(rawValue)) return false;
2467
+ }
2468
+ }
2469
+
2470
+ return defaultValue;
2471
+ }
2472
+
2207
2473
  function readMissionOpsWorkspace({ wrapperRoot, outputDir }) {
2208
2474
  const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
2209
2475
  const missionOpsPath = path.join(dataRoot, "mission-ops.yml");
@@ -3108,13 +3374,9 @@ async function runFeedbackSync(args) {
3108
3374
 
3109
3375
  const timeoutMs = resolveTimeoutMs(args);
3110
3376
  const apiBase = resolveApiBase(args);
3111
- const includePrdText = args["with-prd-text"] !== undefined
3112
- ? Boolean(args["with-prd-text"])
3113
- : args.withPrdText !== undefined
3114
- ? Boolean(args.withPrdText)
3115
- : true;
3377
+ const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
3116
3378
  const filters = {
3117
- status: firstNonEmptyString(args.status) || "Pending",
3379
+ status: firstNonEmptyString(args.status) || "",
3118
3380
  source: firstNonEmptyString(args.source) || "",
3119
3381
  includePrdText,
3120
3382
  includeCommentTurns: true,
@@ -3815,7 +4077,46 @@ async function runQuery(args) {
3815
4077
 
3816
4078
  let data;
3817
4079
  try {
3818
- data = await callAssistantQuery({ apiBase, key, payload, timeoutMs });
4080
+ const queued = await createAssistantQueryJob({ apiBase, key, payload, timeoutMs });
4081
+ if (queued.answer) {
4082
+ data = queued;
4083
+ } else {
4084
+ const jobId = firstNonEmptyString(queued.job_id, queued.id);
4085
+ if (!jobId) {
4086
+ throw new Error("Query job was accepted without a job_id.");
4087
+ }
4088
+ const pollTimeoutMs = Math.max(timeoutMs, 900_000);
4089
+ const startedAt = Date.now();
4090
+ let finalStatus = null;
4091
+ do {
4092
+ await sleep(2_000);
4093
+ try {
4094
+ finalStatus = await fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId });
4095
+ } catch (err) {
4096
+ if (Number(err?.status) === 429) {
4097
+ await sleep(resolveRetryAfterMs(err));
4098
+ continue;
4099
+ }
4100
+ throw err;
4101
+ }
4102
+ if (["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
4103
+ break;
4104
+ }
4105
+ } while (Date.now() - startedAt < pollTimeoutMs);
4106
+
4107
+ if (!finalStatus || !["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
4108
+ throw new Error(`Timed out waiting for query job ${jobId}`);
4109
+ }
4110
+ if (String(finalStatus.status || "").trim() === "failed") {
4111
+ const detail = firstNonEmptyString(finalStatus?.error?.message, finalStatus?.error?.code);
4112
+ throw new Error(detail || `Query job ${jobId} failed`);
4113
+ }
4114
+ data = {
4115
+ answer: finalStatus.answer,
4116
+ context_blocks: finalStatus.context_blocks,
4117
+ telemetry: finalStatus.telemetry,
4118
+ };
4119
+ }
3819
4120
  } catch (err) {
3820
4121
  if (err?.name === "AbortError") {
3821
4122
  console.error(`Request timed out after ${timeoutMs}ms`);
@@ -3847,6 +4148,10 @@ async function main() {
3847
4148
  console.error(REMOVED_COMMAND_MESSAGES[command]);
3848
4149
  process.exit(1);
3849
4150
  }
4151
+ if (command === "mission") {
4152
+ console.error("Unknown mission command. Use `myte mission status --mission-ids \"M001,M002\" --status todo|in_progress|done`.");
4153
+ process.exit(1);
4154
+ }
3850
4155
  const args = parseArgs(rest);
3851
4156
  if (args.help || command === "help") {
3852
4157
  printHelp();
@@ -3873,6 +4178,11 @@ async function main() {
3873
4178
  return;
3874
4179
  }
3875
4180
 
4181
+ if (command === "mission-status") {
4182
+ await runMissionStatus(args);
4183
+ return;
4184
+ }
4185
+
3876
4186
  if (command === "sync-qaqc" || command === "qaqc-sync") {
3877
4187
  await runSyncQaqc(args);
3878
4188
  return;
package/lib/ai-gateway.js CHANGED
@@ -16,7 +16,10 @@ function buildSimpleAiPayload({ query, jsonResponse = false, maxOutputTokens, te
16
16
  const payload = {
17
17
  messages: [{ role: "user", content: String(query || "").trim() }],
18
18
  };
19
- if (jsonResponse) payload.myte_json_response = true;
19
+ if (jsonResponse) {
20
+ payload.myte_json_response = true;
21
+ payload.response_format = { type: "json_object" };
22
+ }
20
23
  if (Number.isFinite(Number(maxOutputTokens)) && Number(maxOutputTokens) > 0) {
21
24
  payload.max_tokens = Number(maxOutputTokens);
22
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Myte CLI core implementation (Project Assistant + Myte AI gateway).",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",