@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 +10 -0
- package/cli.js +341 -31
- package/lib/ai-gateway.js +4 -1
- package/package.json +1 -1
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
|
|
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
|
-
" -
|
|
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>
|
|
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
|
|
295
|
-
" --
|
|
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 --
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
|
1893
|
-
|
|
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
|
|
1897
|
-
|
|
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
|
|
1901
|
-
|
|
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
|
|
1905
|
-
|
|
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
|
|
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) || "
|
|
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
|
-
|
|
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)
|
|
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
|
}
|