@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 +4 -1
- package/cli.js +260 -29
- package/lib/ai-gateway.js +4 -1
- package/package.json +1 -1
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`
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
|
1851
|
-
|
|
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
|
|
1855
|
-
|
|
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
|
|
1859
|
-
|
|
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
|
|
1863
|
-
|
|
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 ||
|
|
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:
|
|
1939
|
-
|
|
1940
|
-
:
|
|
1941
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
}
|