@mytegroupinc/myte-core 0.0.15 → 0.0.17

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
@@ -76,7 +76,8 @@ Notes:
76
76
  - `sync-qaqc` keeps QAQC state in one deterministic file so the working set grows and shrinks with current active-mission reality.
77
77
  - `sync-qaqc` fully rewrites `MyteCommandCenter/data/qaqc.yml` on every sync and does not delete `MyteCommandCenter/data/missions/*.yml`.
78
78
  - `feedback-sync` writes one deterministic feedback snapshot under `MyteCommandCenter/data/feedback.yml`.
79
- - `feedback-sync` includes readable PRD text inline when PRD text exists.
79
+ - `feedback-sync` keeps feedback metadata plus comment turns in `MyteCommandCenter/data/feedback.yml`.
80
+ - `feedback-sync` writes full PRD context into `MyteCommandCenter/PRD/feedback-sync/*.md` and points to those files from `feedback.yml`.
80
81
  - `feedback-sync` fully replaces the feedback-owned sync file to avoid stale local feedback noise.
81
82
  - `feedback-sync` fully rewrites `MyteCommandCenter/data/feedback.yml` on every sync and does not delete `MyteCommandCenter/data/missions/*.yml`.
82
83
  - `suggestions sync` writes one merge-safe workflow file at `MyteCommandCenter/data/mission-ops.yml`.
@@ -138,3 +139,5 @@ Examples:
138
139
  - `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
139
140
 
140
141
  This package is published under the org scope for governance; the public `myte` wrapper delegates here.
142
+
143
+ `--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
@@ -266,7 +266,8 @@ function printHelp() {
266
266
  "",
267
267
  "feedback-sync contract:",
268
268
  " - Runs from the wrapper root that contains the project's configured repo folders",
269
- " - Writes open project feedback and inline PRD context into one deterministic file: MyteCommandCenter/data/feedback.yml",
269
+ " - Writes open project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
270
+ " - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
270
271
  "",
271
272
  "Options:",
272
273
  " --with-diff Include deterministic git diffs (project-scoped)",
@@ -274,7 +275,7 @@ function printHelp() {
274
275
  " --timeout-ms <ms> Request timeout (default: 300000)",
275
276
  " --base-url <url> API base (default: https://api.myte.dev)",
276
277
  " --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
277
- " --json-response Ask the Myte AI gateway to return clean JSON only",
278
+ " --json-response Ask the Myte AI gateway to return clean JSON only and send OpenAI-compatible response_format",
278
279
  " --max-output-tokens Output token cap for `myte ai` simple queries",
279
280
  " --temperature <num> Temperature for `myte ai` simple queries",
280
281
  " --output-dir <path> Command Center output directory (default: <wrapper-root>/MyteCommandCenter)",
@@ -290,7 +291,7 @@ function printHelp() {
290
291
  " --target-contact-ids Comma-separated client contact ObjectIds",
291
292
  " --status <value> Feedback status filter for feedback-sync (default: Pending)",
292
293
  " --source <value> Feedback source filter for feedback-sync",
293
- " --with-prd-text Include extracted PRD text in feedback-sync (default: on)",
294
+ " --with-prd-text Include extracted PRD text so local PRD files can be materialized during feedback-sync (default: on)",
294
295
  " --mission-ids <ids> Comma-separated mission business ids for run-qaqc (quote multi-id values on PowerShell)",
295
296
  " --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
296
297
  " --wait Poll batch status until terminal completion for run-qaqc",
@@ -1066,9 +1067,15 @@ async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
1066
1067
  );
1067
1068
 
1068
1069
  if (!resp.ok || body.status !== "success") {
1070
+ const retryAfter = resp.headers?.get?.("retry-after");
1069
1071
  const msg = body?.message || `QAQC sync request failed (${resp.status})`;
1070
- const err = new Error(msg);
1072
+ const err = new Error(
1073
+ retryAfter
1074
+ ? `${msg} Retry after ${retryAfter}s.`
1075
+ : msg
1076
+ );
1071
1077
  err.status = resp.status;
1078
+ if (retryAfter) err.retryAfter = retryAfter;
1072
1079
  throw err;
1073
1080
  }
1074
1081
  return body.data || {};
@@ -1082,6 +1089,9 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
1082
1089
  if (filters.includePrdText !== undefined) {
1083
1090
  url.searchParams.set("include_prd_text", filters.includePrdText ? "true" : "false");
1084
1091
  }
1092
+ if (filters.includeCommentTurns !== undefined) {
1093
+ url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
1094
+ }
1085
1095
  const { resp, body } = await fetchJsonWithTimeout(
1086
1096
  fetchFn,
1087
1097
  url.toString(),
@@ -1223,15 +1233,30 @@ async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
1223
1233
  );
1224
1234
 
1225
1235
  if (!resp.ok || body.status !== "success") {
1236
+ const retryAfter = resp.headers?.get?.("retry-after");
1226
1237
  const msg = body?.message || `Run QAQC status request failed (${resp.status})`;
1227
- const err = new Error(msg);
1238
+ const err = new Error(
1239
+ retryAfter
1240
+ ? `${msg} Retry after ${retryAfter}s.`
1241
+ : msg
1242
+ );
1228
1243
  err.status = resp.status;
1244
+ if (retryAfter) err.retryAfter = retryAfter;
1229
1245
  throw err;
1230
1246
  }
1231
1247
  return body.data || {};
1232
1248
  }
1233
1249
 
1234
- async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
1250
+ function resolveRetryAfterMs(err, fallbackMs = 5_000) {
1251
+ const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
1252
+ const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
1253
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
1254
+ return Math.min(retryAfterSeconds * 1_000, 60_000);
1255
+ }
1256
+ return fallbackMs;
1257
+ }
1258
+
1259
+ async function createAssistantQueryJob({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
1235
1260
  const fetchFn = await getFetch();
1236
1261
  const url = `${apiBase}${endpoint}`;
1237
1262
  const { resp, body } = await fetchJsonWithTimeout(
@@ -1249,9 +1274,35 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint =
1249
1274
  );
1250
1275
 
1251
1276
  if (!resp.ok || body.status !== "success") {
1277
+ const retryAfter = resp.headers?.get?.("retry-after");
1252
1278
  const msg = body?.message || `Query failed (${resp.status})`;
1253
- const err = new Error(msg);
1279
+ const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
1280
+ err.status = resp.status;
1281
+ if (retryAfter) err.retryAfter = retryAfter;
1282
+ throw err;
1283
+ }
1284
+ return body.data || {};
1285
+ }
1286
+
1287
+ async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId }) {
1288
+ const fetchFn = await getFetch();
1289
+ const url = `${apiBase}/project-assistant/query/${encodeURIComponent(String(jobId || ""))}`;
1290
+ const { resp, body } = await fetchJsonWithTimeout(
1291
+ fetchFn,
1292
+ url,
1293
+ {
1294
+ method: "GET",
1295
+ headers: { Authorization: `Bearer ${key}` },
1296
+ },
1297
+ timeoutMs
1298
+ );
1299
+
1300
+ if (!resp.ok || body.status !== "success") {
1301
+ const retryAfter = resp.headers?.get?.("retry-after");
1302
+ const msg = body?.message || `Query status request failed (${resp.status})`;
1303
+ const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
1254
1304
  err.status = resp.status;
1305
+ if (retryAfter) err.retryAfter = retryAfter;
1255
1306
  throw err;
1256
1307
  }
1257
1308
  return body.data || {};
@@ -1322,6 +1373,10 @@ async function runRunQaqc(args) {
1322
1373
  try {
1323
1374
  finalStatus = await fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId: data.batch_id });
1324
1375
  } catch (err) {
1376
+ if (Number(err?.status) === 429) {
1377
+ await sleep(resolveRetryAfterMs(err));
1378
+ continue;
1379
+ }
1325
1380
  console.error("Failed to fetch QAQC batch status:", err?.message || err);
1326
1381
  process.exit(1);
1327
1382
  }
@@ -1337,11 +1392,18 @@ async function runRunQaqc(args) {
1337
1392
 
1338
1393
  if (shouldSync && ["completed", "completed_with_errors"].includes(String(finalStatus.status || "").trim())) {
1339
1394
  let snapshot;
1340
- try {
1341
- snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
1342
- } catch (err) {
1343
- console.error("Failed to fetch QAQC sync snapshot after batch completion:", err?.message || err);
1344
- process.exit(1);
1395
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1396
+ try {
1397
+ snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
1398
+ break;
1399
+ } catch (err) {
1400
+ if (Number(err?.status) === 429 && attempt < 4) {
1401
+ await sleep(resolveRetryAfterMs(err));
1402
+ continue;
1403
+ }
1404
+ console.error("Failed to fetch QAQC sync snapshot after batch completion:", err?.message || err);
1405
+ process.exit(1);
1406
+ }
1345
1407
  }
1346
1408
 
1347
1409
  let resolved;
@@ -1664,6 +1726,20 @@ function stableItemId(item, keys, fallback) {
1664
1726
  return fallback;
1665
1727
  }
1666
1728
 
1729
+ function bootstrapPathId(item, preferredKeys, fallbackKeys, fallback) {
1730
+ return stableItemId(item, preferredKeys, stableItemId(item, fallbackKeys, fallback));
1731
+ }
1732
+
1733
+ function bootstrapScopedPathId(item, preferredKeys, scopeKeys, fallbackKeys, fallback) {
1734
+ const preferred = stableItemId(item, preferredKeys, "");
1735
+ if (preferred) return preferred;
1736
+ const fallbackId = stableItemId(item, fallbackKeys, fallback);
1737
+ const scopeParts = (Array.isArray(scopeKeys) ? scopeKeys : [])
1738
+ .map((key) => String(item?.[key] || "").trim())
1739
+ .filter(Boolean);
1740
+ return [...scopeParts, fallbackId].filter(Boolean).join("__") || fallbackId;
1741
+ }
1742
+
1667
1743
  function stringifyYaml(value) {
1668
1744
  // eslint-disable-next-line global-require
1669
1745
  const YAML = require("yaml");
@@ -1704,6 +1780,44 @@ function writeTextFile(filePath, value) {
1704
1780
  fs.writeFileSync(filePath, String(value || ""), "utf8");
1705
1781
  }
1706
1782
 
1783
+ function sanitizeFileSegment(value, fallback = "item") {
1784
+ const cleaned = String(value || "")
1785
+ .trim()
1786
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
1787
+ .replace(/\s+/g, "-")
1788
+ .replace(/-+/g, "-")
1789
+ .replace(/^\.+|\.+$/g, "")
1790
+ .replace(/^-+|-+$/g, "");
1791
+ return cleaned || fallback;
1792
+ }
1793
+
1794
+ function toPosixRelativePath(rootPath, targetPath) {
1795
+ return path.relative(rootPath, targetPath).split(path.sep).join("/");
1796
+ }
1797
+
1798
+ function ensureTrailingNewline(value) {
1799
+ const text = String(value || "");
1800
+ if (!text) return "";
1801
+ return text.endsWith("\n") ? text : `${text}\n`;
1802
+ }
1803
+
1804
+ function normalizeFeedbackConversationTurns(turns) {
1805
+ if (!Array.isArray(turns)) return [];
1806
+ return turns
1807
+ .map((turn) => {
1808
+ const content = String(turn?.content || "").trim();
1809
+ if (!content) return null;
1810
+ const normalized = {
1811
+ sender_name: firstNonEmptyString(turn?.sender_name, turn?.user_name, turn?.author_name) || "Unknown",
1812
+ content,
1813
+ };
1814
+ const createdAt = firstNonEmptyString(turn?.created_at, turn?.timestamp);
1815
+ if (createdAt) normalized.created_at = createdAt;
1816
+ return normalized;
1817
+ })
1818
+ .filter(Boolean);
1819
+ }
1820
+
1707
1821
  function readJsonFile(filePath) {
1708
1822
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
1709
1823
  try {
@@ -1847,20 +1961,42 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
1847
1961
  const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
1848
1962
 
1849
1963
  phases.forEach((phase, index) => {
1850
- const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
1851
- writeYamlFile(path.join(phasesDir, `${phaseId}.yml`), phase);
1964
+ const phasePathId = bootstrapPathId(
1965
+ phase,
1966
+ ["phase_key", "id"],
1967
+ ["phase_id"],
1968
+ `phase-${String(index + 1).padStart(3, "0")}`
1969
+ );
1970
+ writeYamlFile(path.join(phasesDir, `${phasePathId}.yml`), phase);
1852
1971
  });
1853
1972
  epics.forEach((epic, index) => {
1854
- const epicId = stableItemId(epic, ["epic_id", "id"], `E${String(index + 1).padStart(3, "0")}`);
1855
- writeYamlFile(path.join(epicsDir, `${epicId}.yml`), epic);
1973
+ const epicPathId = bootstrapScopedPathId(
1974
+ epic,
1975
+ ["epic_key", "id"],
1976
+ ["phase_key", "phase_id"],
1977
+ ["epic_id"],
1978
+ `epic-${String(index + 1).padStart(3, "0")}`
1979
+ );
1980
+ writeYamlFile(path.join(epicsDir, `${epicPathId}.yml`), epic);
1856
1981
  });
1857
1982
  stories.forEach((story, index) => {
1858
- const storyId = stableItemId(story, ["story_id", "id"], `S${String(index + 1).padStart(3, "0")}`);
1859
- writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
1983
+ const storyPathId = bootstrapScopedPathId(
1984
+ story,
1985
+ ["story_key", "id"],
1986
+ ["phase_key", "phase_id", "epic_key", "epic_id"],
1987
+ ["story_id"],
1988
+ `story-${String(index + 1).padStart(3, "0")}`
1989
+ );
1990
+ writeYamlFile(path.join(storiesDir, `${storyPathId}.yml`), story);
1860
1991
  });
1861
1992
  missions.forEach((mission, index) => {
1862
- const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
1863
- writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
1993
+ const missionPathId = bootstrapPathId(
1994
+ mission,
1995
+ ["mission_key", "id"],
1996
+ ["mission_id"],
1997
+ `mission-${String(index + 1).padStart(3, "0")}`
1998
+ );
1999
+ writeYamlFile(path.join(missionsDir, `${missionPathId}.yml`), mission);
1864
2000
  });
1865
2001
 
1866
2002
  if (snapshot.project && typeof snapshot.project === "object") {
@@ -1921,8 +2057,11 @@ function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
1921
2057
 
1922
2058
  function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
1923
2059
  const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
2060
+ const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
1924
2061
 
1925
2062
  ensureDir(dataRoot);
2063
+ ensureDir(prdSyncDir);
2064
+ clearFileDirectory(prdSyncDir, [".md", ".markdown", ".txt"]);
1926
2065
  pruneLegacyCommandCenterArtifacts(dataRoot, { bootstrap: true, feedback: true });
1927
2066
 
1928
2067
  if (snapshot.project && typeof snapshot.project === "object") {
@@ -1930,15 +2069,58 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
1930
2069
  }
1931
2070
 
1932
2071
  const items = Array.isArray(snapshot.items) ? snapshot.items : [];
2072
+ let prdFileCount = 0;
2073
+ const materializedItems = items.map((rawItem, index) => {
2074
+ const item = isPlainObject(rawItem) ? { ...rawItem } : {};
2075
+ const feedbackId = stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`);
2076
+ const conversationTurns = normalizeFeedbackConversationTurns(item.conversation_turns);
2077
+ const prdText = String(item.prd_text || "").trim();
2078
+
2079
+ let contextSource = "description_only";
2080
+ let contextNote = "No separate PRD. Use feedback_text as the context for this feedback item.";
2081
+ let prdFile = null;
2082
+
2083
+ if (prdText) {
2084
+ const prdFilename = `${sanitizeFileSegment(feedbackId, `feedback-${index + 1}`)}.md`;
2085
+ const prdPath = path.join(prdSyncDir, prdFilename);
2086
+ writeTextFile(prdPath, ensureTrailingNewline(prdText));
2087
+ prdFile = toPosixRelativePath(targetRoot, prdPath);
2088
+ contextSource = "prd_file";
2089
+ contextNote = "Full PRD context is stored in the linked file.";
2090
+ prdFileCount += 1;
2091
+ } else if (item.has_prd_text) {
2092
+ contextSource = "prd_declared_but_unavailable";
2093
+ contextNote = "A separate PRD exists for this feedback item, but readable PRD text was not included in this sync snapshot.";
2094
+ }
2095
+
2096
+ return {
2097
+ ...item,
2098
+ feedback_id: feedbackId,
2099
+ conversation_turns: conversationTurns,
2100
+ context_source: contextSource,
2101
+ context_note: contextNote,
2102
+ prd_file: prdFile,
2103
+ };
2104
+ });
2105
+ const snapshotCounts = snapshot.counts && typeof snapshot.counts === "object"
2106
+ ? { ...snapshot.counts }
2107
+ : { total_feedback: materializedItems.length };
2108
+ snapshotCounts.with_prd_files = prdFileCount;
2109
+ if (snapshotCounts.with_conversation_turns === undefined) {
2110
+ snapshotCounts.with_conversation_turns = materializedItems.filter((item) => Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0).length;
2111
+ }
2112
+
1933
2113
  const payload = scrubBootstrapValue({
1934
- schema_version: snapshot.schema_version || 1,
2114
+ schema_version: snapshot.schema_version || 2,
1935
2115
  project: snapshot.project || null,
1936
2116
  repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
1937
2117
  filters: snapshot.filters && typeof snapshot.filters === "object" ? snapshot.filters : {},
1938
- counts: snapshot.counts && typeof snapshot.counts === "object"
1939
- ? snapshot.counts
1940
- : { total_feedback: items.length },
1941
- queue: items
2118
+ counts: snapshotCounts,
2119
+ artifacts: {
2120
+ feedback_prd_root: "PRD/feedback-sync",
2121
+ with_prd_files: prdFileCount,
2122
+ },
2123
+ queue: materializedItems
1942
2124
  .filter((item) => String(item?.status || "").trim().toLowerCase() !== "resolved")
1943
2125
  .map((item, index) => ({
1944
2126
  feedback_id: stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`),
@@ -1946,7 +2128,11 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
1946
2128
  priority: item?.priority || null,
1947
2129
  title: item?.title || null,
1948
2130
  })),
1949
- items,
2131
+ items: materializedItems.map((item) => {
2132
+ const nextItem = { ...item };
2133
+ delete nextItem.prd_text;
2134
+ return nextItem;
2135
+ }),
1950
2136
  pagination: snapshot.pagination && typeof snapshot.pagination === "object" ? snapshot.pagination : undefined,
1951
2137
  generated_at: snapshot.generated_at || null,
1952
2138
  snapshot_hash: snapshot.snapshot_hash || null,
@@ -1956,6 +2142,7 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
1956
2142
  return {
1957
2143
  targetRoot,
1958
2144
  dataRoot,
2145
+ prdSyncDir,
1959
2146
  manifest: payload,
1960
2147
  };
1961
2148
  }
@@ -3024,6 +3211,7 @@ async function runFeedbackSync(args) {
3024
3211
  status: firstNonEmptyString(args.status) || "Pending",
3025
3212
  source: firstNonEmptyString(args.source) || "",
3026
3213
  includePrdText,
3214
+ includeCommentTurns: true,
3027
3215
  };
3028
3216
 
3029
3217
  let snapshot;
@@ -3079,7 +3267,7 @@ async function runFeedbackSync(args) {
3079
3267
  console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
3080
3268
  console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
3081
3269
  if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
3082
- console.log(`Counts: total_feedback=${summary.counts.total_feedback || 0}, with_prd_text=${summary.counts.with_prd_text || 0}`);
3270
+ console.log(`Counts: total_feedback=${summary.counts.total_feedback || 0}, with_prd_text=${summary.counts.with_prd_text || 0}, with_conversation_turns=${summary.counts.with_conversation_turns || 0}`);
3083
3271
  console.log("Dry run only - no files written.");
3084
3272
  }
3085
3273
  return;
@@ -3087,6 +3275,9 @@ async function runFeedbackSync(args) {
3087
3275
 
3088
3276
  const writeResult = writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir });
3089
3277
  summary.data_root = writeResult.dataRoot;
3278
+ summary.prd_root = writeResult.prdSyncDir;
3279
+ summary.counts = writeResult.manifest?.counts || summary.counts;
3280
+ summary.artifacts = writeResult.manifest?.artifacts || null;
3090
3281
 
3091
3282
  if (args.json) {
3092
3283
  console.log(JSON.stringify(summary, null, 2));
@@ -3099,7 +3290,8 @@ async function runFeedbackSync(args) {
3099
3290
  console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
3100
3291
  console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
3101
3292
  if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
3102
- console.log(`Wrote feedback: total_feedback=${summary.counts.total_feedback || 0}, with_prd_text=${summary.counts.with_prd_text || 0}`);
3293
+ console.log(`Wrote feedback: total_feedback=${summary.counts.total_feedback || 0}, with_prd_text=${summary.counts.with_prd_text || 0}, with_conversation_turns=${summary.counts.with_conversation_turns || 0}`);
3294
+ console.log(`PRD root: ${summary.prd_root}`);
3103
3295
  console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
3104
3296
  }
3105
3297
 
@@ -3717,7 +3909,46 @@ async function runQuery(args) {
3717
3909
 
3718
3910
  let data;
3719
3911
  try {
3720
- data = await callAssistantQuery({ apiBase, key, payload, timeoutMs });
3912
+ const queued = await createAssistantQueryJob({ apiBase, key, payload, timeoutMs });
3913
+ if (queued.answer) {
3914
+ data = queued;
3915
+ } else {
3916
+ const jobId = firstNonEmptyString(queued.job_id, queued.id);
3917
+ if (!jobId) {
3918
+ throw new Error("Query job was accepted without a job_id.");
3919
+ }
3920
+ const pollTimeoutMs = Math.max(timeoutMs, 900_000);
3921
+ const startedAt = Date.now();
3922
+ let finalStatus = null;
3923
+ do {
3924
+ await sleep(2_000);
3925
+ try {
3926
+ finalStatus = await fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId });
3927
+ } catch (err) {
3928
+ if (Number(err?.status) === 429) {
3929
+ await sleep(resolveRetryAfterMs(err));
3930
+ continue;
3931
+ }
3932
+ throw err;
3933
+ }
3934
+ if (["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
3935
+ break;
3936
+ }
3937
+ } while (Date.now() - startedAt < pollTimeoutMs);
3938
+
3939
+ if (!finalStatus || !["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
3940
+ throw new Error(`Timed out waiting for query job ${jobId}`);
3941
+ }
3942
+ if (String(finalStatus.status || "").trim() === "failed") {
3943
+ const detail = firstNonEmptyString(finalStatus?.error?.message, finalStatus?.error?.code);
3944
+ throw new Error(detail || `Query job ${jobId} failed`);
3945
+ }
3946
+ data = {
3947
+ answer: finalStatus.answer,
3948
+ context_blocks: finalStatus.context_blocks,
3949
+ telemetry: finalStatus.telemetry,
3950
+ };
3951
+ }
3721
3952
  } catch (err) {
3722
3953
  if (err?.name === "AbortError") {
3723
3954
  console.error(`Request timed out after ${timeoutMs}ms`);
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.15",
3
+ "version": "0.0.17",
4
4
  "description": "Myte CLI core implementation (Project Assistant + Myte AI gateway).",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",