@mytegroupinc/myte-core 0.0.24 → 0.0.27
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 +105 -0
- package/cli.js +484 -126
- package/package.json +4 -5
- package/scripts/mission-live-disposable-harness.js +226 -0
- package/scripts/mission-live-full-harness.js +824 -0
package/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ const REMOVED_COMMAND_MESSAGES = {
|
|
|
30
30
|
"add-prd": "The `add-prd` alias has been removed. Use `myte create-prd <file.md> [more.md ...]`.",
|
|
31
31
|
prd: "The `prd` alias has been removed. Use `myte create-prd <file.md> [more.md ...]`.",
|
|
32
32
|
"qaqc-status-update": "The `qaqc-status-update` command has been removed. Use `myte mission status --mission-ids \"M001,M002\" --status todo|in_progress|done`.",
|
|
33
|
+
"mission-restore": "The project-key `myte mission restore` command has been removed. Restore archived missions from the Myte web archived missions board.",
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
function findEnvPath(startDir) {
|
|
@@ -46,25 +47,25 @@ function findEnvPath(startDir) {
|
|
|
46
47
|
|
|
47
48
|
function loadEnv() {
|
|
48
49
|
const envPath = findEnvPath(process.cwd());
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return;
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}
|
|
50
|
+
if (!envPath || !fs.existsSync(envPath)) return;
|
|
51
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
52
|
+
content.split(/\r?\n/).forEach((line) => {
|
|
53
|
+
const trimmed = String(line || "").trim();
|
|
54
|
+
if (!trimmed || trimmed.startsWith("#")) return;
|
|
55
|
+
const idx = trimmed.indexOf("=");
|
|
56
|
+
if (idx === -1) return;
|
|
57
|
+
const key = trimmed.slice(0, idx).trim();
|
|
58
|
+
let val = trimmed.slice(idx + 1).trim();
|
|
59
|
+
if (
|
|
60
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
61
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
62
|
+
) {
|
|
63
|
+
val = val.slice(1, -1);
|
|
64
|
+
}
|
|
65
|
+
if (key && !(key in process.env)) {
|
|
66
|
+
process.env[key] = val;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function splitCommand(argv) {
|
|
@@ -75,6 +76,12 @@ function splitCommand(argv) {
|
|
|
75
76
|
if (argv[1] === "status") {
|
|
76
77
|
return { command: "mission-status", rest: argv.slice(2) };
|
|
77
78
|
}
|
|
79
|
+
if (argv[1] === "archive") {
|
|
80
|
+
return { command: "mission-archive", rest: argv.slice(2) };
|
|
81
|
+
}
|
|
82
|
+
if (argv[1] === "restore" || argv[1] === "unarchive") {
|
|
83
|
+
return { command: "mission-restore", rest: argv.slice(2) };
|
|
84
|
+
}
|
|
78
85
|
return { command: "mission", rest: argv.slice(1) };
|
|
79
86
|
}
|
|
80
87
|
|
|
@@ -113,105 +120,60 @@ function splitCommand(argv) {
|
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
function parseArgs(argv) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"target-contact-ids",
|
|
144
|
-
"status",
|
|
145
|
-
"source",
|
|
146
|
-
"feedback-id",
|
|
147
|
-
"request-id",
|
|
148
|
-
"event-id",
|
|
149
|
-
"version-id",
|
|
150
|
-
"compare-to",
|
|
151
|
-
"action",
|
|
152
|
-
"decision",
|
|
153
|
-
"to-state",
|
|
154
|
-
"from-state",
|
|
155
|
-
"reason",
|
|
156
|
-
"review-action",
|
|
157
|
-
"idempotency-key",
|
|
158
|
-
"user-id",
|
|
159
|
-
"assigned-user-id",
|
|
160
|
-
"due-date",
|
|
161
|
-
"priority",
|
|
162
|
-
"review-note",
|
|
163
|
-
"final-file",
|
|
164
|
-
"tags",
|
|
165
|
-
"tag",
|
|
166
|
-
"mission-ids",
|
|
167
|
-
"client-session-id",
|
|
168
|
-
],
|
|
169
|
-
alias: {
|
|
170
|
-
q: "query",
|
|
171
|
-
d: "with-diff",
|
|
172
|
-
c: "context",
|
|
173
|
-
h: "help",
|
|
174
|
-
},
|
|
175
|
-
default: {
|
|
176
|
-
fetch: true,
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
parsed.__raw = Array.isArray(argv) ? [...argv] : [];
|
|
180
|
-
return parsed;
|
|
181
|
-
} catch (err) {
|
|
182
|
-
const parsed = { _: [] };
|
|
183
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
184
|
-
const token = argv[i];
|
|
185
|
-
if (token.startsWith("--no-")) {
|
|
186
|
-
const key = token.slice(5);
|
|
187
|
-
parsed[key] = false;
|
|
123
|
+
const parsed = { _: [], fetch: true };
|
|
124
|
+
const aliases = { q: "query", d: "with-diff", c: "context", h: "help" };
|
|
125
|
+
const setValue = (key, value) => {
|
|
126
|
+
const finalKey = aliases[key] || key;
|
|
127
|
+
if (parsed[finalKey] === undefined) {
|
|
128
|
+
parsed[finalKey] = value;
|
|
129
|
+
} else if (Array.isArray(parsed[finalKey])) {
|
|
130
|
+
parsed[finalKey].push(value);
|
|
131
|
+
} else {
|
|
132
|
+
parsed[finalKey] = [parsed[finalKey], value];
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
137
|
+
const token = argv[i];
|
|
138
|
+
if (token === "--") {
|
|
139
|
+
parsed._.push(...argv.slice(i + 1));
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (token.startsWith("--no-")) {
|
|
143
|
+
setValue(token.slice(5), false);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (token.startsWith("--")) {
|
|
147
|
+
const eqIdx = token.indexOf("=");
|
|
148
|
+
if (eqIdx !== -1) {
|
|
149
|
+
setValue(token.slice(2, eqIdx), token.slice(eqIdx + 1));
|
|
188
150
|
continue;
|
|
189
151
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
i += 1;
|
|
196
|
-
} else {
|
|
197
|
-
parsed[key] = true;
|
|
198
|
-
}
|
|
199
|
-
} else if (token.startsWith("-")) {
|
|
200
|
-
const key = token.slice(1);
|
|
201
|
-
const next = argv[i + 1];
|
|
202
|
-
if (next && !next.startsWith("-")) {
|
|
203
|
-
parsed[key] = next;
|
|
204
|
-
i += 1;
|
|
205
|
-
} else {
|
|
206
|
-
parsed[key] = true;
|
|
207
|
-
}
|
|
152
|
+
const key = token.slice(2);
|
|
153
|
+
const next = argv[i + 1];
|
|
154
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
155
|
+
setValue(key, next);
|
|
156
|
+
i += 1;
|
|
208
157
|
} else {
|
|
209
|
-
|
|
158
|
+
setValue(key, true);
|
|
210
159
|
}
|
|
160
|
+
continue;
|
|
211
161
|
}
|
|
212
|
-
|
|
213
|
-
|
|
162
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
163
|
+
const key = token.slice(1);
|
|
164
|
+
const next = argv[i + 1];
|
|
165
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
166
|
+
setValue(key, next);
|
|
167
|
+
i += 1;
|
|
168
|
+
} else {
|
|
169
|
+
setValue(key, true);
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
parsed._.push(token);
|
|
214
174
|
}
|
|
175
|
+
parsed.__raw = Array.isArray(argv) ? [...argv] : [];
|
|
176
|
+
return parsed;
|
|
215
177
|
}
|
|
216
178
|
|
|
217
179
|
function printHelp() {
|
|
@@ -224,7 +186,8 @@ function printHelp() {
|
|
|
224
186
|
" myte config [--json]",
|
|
225
187
|
" myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
|
|
226
188
|
" myte run-qaqc --mission-ids \"M001[,M002...]\" [--wait] [--sync] [--force] [--json]",
|
|
227
|
-
" myte mission status --mission-ids \"M001[,M002...]\" --status todo|in_progress|done [--json]",
|
|
189
|
+
" myte mission status --mission-ids \"M001[,M002...]\" --status todo|in_progress|done [--no-sync] [--json]",
|
|
190
|
+
" myte mission archive --mission-ids \"M001[,M002...]\" [--reason \"...\"] [--no-sync] [--json]",
|
|
228
191
|
" myte sync-qaqc [--output-dir ./MyteCommandCenter] [--json]",
|
|
229
192
|
" myte suggestions sync [--output-dir ./MyteCommandCenter] [--json]",
|
|
230
193
|
" myte suggestions create [--file ./payload.yml] [--no-sync] [--json]",
|
|
@@ -272,8 +235,10 @@ function printHelp() {
|
|
|
272
235
|
"bootstrap contract:",
|
|
273
236
|
" - Run from any workspace where you want local MyteCommandCenter data written",
|
|
274
237
|
" - Writes MyteCommandCenter/data/project.yml plus phases, epics, stories, and missions locally",
|
|
238
|
+
" - Also refreshes mission review threads into MyteCommandCenter/data/mission-ops.yml by default",
|
|
275
239
|
" - Uses the project-scoped bootstrap snapshot from the Myte API",
|
|
276
240
|
" - Mission cards include richer execution context like complexity, estimated_hours, due_date, subtasks, technical_requirements, resources_needed, labels, and normalized test_cases",
|
|
241
|
+
" - Use --no-suggestions only when you intentionally want mission cards without mission review-thread state",
|
|
277
242
|
"",
|
|
278
243
|
"sync-qaqc contract:",
|
|
279
244
|
" - Run from any workspace where you want local MyteCommandCenter data written",
|
|
@@ -284,7 +249,10 @@ function printHelp() {
|
|
|
284
249
|
" - suggestions sync pulls /api/project-assistant/suggestions and writes only MyteCommandCenter/data/mission-ops.yml",
|
|
285
250
|
" - sync preserves top-level workspace blocks and per-thread workspace.<actor_scope> drafts from the existing mission-ops file",
|
|
286
251
|
" - create reads --file YAML/JSON or MyteCommandCenter/data/mission-ops.yml workspace.<actor_scope>.draft_submissions[]",
|
|
252
|
+
" - create change_type values are exactly: update, create",
|
|
253
|
+
" - create update items require mission_id; create new-mission items require change_set.title and change_set.description",
|
|
287
254
|
" - revise reads --file YAML/JSON or threads[].workspace.<actor_scope> blocks marked with draft_status=submit|ready|pending_submit",
|
|
255
|
+
" - revise does not accept a new change_type; it revises an existing suggestion_id and keeps that thread's change_type",
|
|
288
256
|
" - review reads --file YAML/JSON or threads[].workspace.<actor_scope> review_action / final_change_set local review blocks",
|
|
289
257
|
" - create/revise/review sync mission-ops by default after success unless --no-sync is set",
|
|
290
258
|
"",
|
|
@@ -298,13 +266,25 @@ function printHelp() {
|
|
|
298
266
|
" - Accepts mission business ids and normalizes status aliases like todo, in_progress, and done",
|
|
299
267
|
" - Canonical mission statuses are exactly: Todo, In Progress, Done",
|
|
300
268
|
" - This only changes mission state. It does not queue QAQC and it does not sync MyteCommandCenter/data/qaqc.yml",
|
|
269
|
+
" - Status updates refresh bootstrap + mission-ops locally by default after success unless --no-sync is set",
|
|
270
|
+
" - Use mission archive for lifecycle archival; do not send Archived through mission status",
|
|
271
|
+
"",
|
|
272
|
+
"mission archive contract:",
|
|
273
|
+
" - Archive calls /api/project-assistant/mission-archive",
|
|
274
|
+
" - Project Owner or elevated owner_delegate authority is required on the authenticated project key",
|
|
275
|
+
" - Archive hides cards from normal bootstrap/board state and keeps the mission record immutable",
|
|
276
|
+
" - Archive refreshes bootstrap + mission-ops locally by default after success unless --no-sync is set",
|
|
277
|
+
" - Restore is intentionally web-only from the archived missions board",
|
|
278
|
+
" - The project-key surface does not hard-delete mission cards",
|
|
301
279
|
"",
|
|
302
280
|
"create-prd contract:",
|
|
303
281
|
" - Required: valid MYTE_API_KEY, PRD markdown body, title",
|
|
304
282
|
" - Accepts one file or many files per command; multi-file uploads are sent in one deterministic batch request",
|
|
305
283
|
" - Title source: myte-kanban.title, first # heading, or --title",
|
|
306
|
-
" - Description source: myte-kanban.description or --description",
|
|
307
|
-
" -
|
|
284
|
+
" - Description source: myte-kanban.description or --description; this is a short feedback/card summary only",
|
|
285
|
+
" - Never put the full PRD in description. The complete PRD must live in the markdown file body",
|
|
286
|
+
" - Each item carries the full PRD markdown blob as prd_markdown/ticket_markdown",
|
|
287
|
+
" - Backend stores that blob as the renderable PRD document source, mirrors it for PRD text search/sync, and generates a DOCX attachment",
|
|
308
288
|
" - PRD DOCX content: the markdown body is stored verbatim",
|
|
309
289
|
"",
|
|
310
290
|
"update-team contract:",
|
|
@@ -327,6 +307,7 @@ function printHelp() {
|
|
|
327
307
|
" - Syncs pending feedback by default so local Command Center data stays focused on active work",
|
|
328
308
|
" - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
|
|
329
309
|
" - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
|
|
310
|
+
" - Reads existing feedback comments; direct project-key creation of feedback-specific comments is not exposed yet",
|
|
330
311
|
"",
|
|
331
312
|
"feedback review contract:",
|
|
332
313
|
" - Draft commands write review artifacts under MyteCommandCenter/reviews/feedback/*.yml for local IDE diff review",
|
|
@@ -394,12 +375,13 @@ function printHelp() {
|
|
|
394
375
|
" --review-note <text> Review note stored in feedback refinement history",
|
|
395
376
|
" --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
|
|
396
377
|
" --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
|
|
397
|
-
" --mission-ids <ids> Comma-separated mission business ids for run-qaqc or mission
|
|
378
|
+
" --mission-ids <ids> Comma-separated mission business ids for run-qaqc, mission status, or mission archive (quote multi-id values on PowerShell)",
|
|
379
|
+
" --reason <text> Optional reason for governed mission archive or feedback changes",
|
|
398
380
|
" --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
|
|
399
381
|
" --wait Poll batch status until terminal completion for run-qaqc",
|
|
400
382
|
" --sync After run-qaqc completes, refresh local QAQC file",
|
|
401
383
|
" --force Allow run-qaqc to bypass stale-state protection when supported",
|
|
402
|
-
" --no-sync Skip automatic
|
|
384
|
+
" --no-sync Skip automatic post-mutation sync for suggestions or mission status/archive",
|
|
403
385
|
" --print-context Print JSON payload and exit (no query call)",
|
|
404
386
|
" --no-fetch Don't git fetch origin main/master before diff",
|
|
405
387
|
"",
|
|
@@ -414,6 +396,7 @@ function printHelp() {
|
|
|
414
396
|
" myte suggestions review --file ./review.yml",
|
|
415
397
|
" myte run-qaqc --mission-ids \"M001,M002\" --wait --sync",
|
|
416
398
|
" myte mission status --mission-ids \"M001,M002\" --status done",
|
|
399
|
+
" myte mission archive --mission-ids \"M001\" --reason \"Duplicate disposable test mission\"",
|
|
417
400
|
" myte bootstrap --output-dir ./MyteCommandCenter",
|
|
418
401
|
" myte sync-qaqc",
|
|
419
402
|
" myte update-team \"Backend deploy completed; QAQC rerun queued.\"",
|
|
@@ -636,8 +619,7 @@ function sleep(ms) {
|
|
|
636
619
|
|
|
637
620
|
async function getFetch() {
|
|
638
621
|
if (typeof fetch !== "undefined") return fetch;
|
|
639
|
-
|
|
640
|
-
return mod.default;
|
|
622
|
+
throw new Error("Global fetch is unavailable. myte requires Node 18+.");
|
|
641
623
|
}
|
|
642
624
|
|
|
643
625
|
function normalizeApiBase(baseRaw) {
|
|
@@ -2103,6 +2085,34 @@ async function createMissionStatusUpdate({ apiBase, key, timeoutMs, payload, ide
|
|
|
2103
2085
|
return body.data || {};
|
|
2104
2086
|
}
|
|
2105
2087
|
|
|
2088
|
+
async function createMissionArchiveUpdate({ apiBase, key, timeoutMs, endpoint, payload, idempotencyKey, clientSessionId }) {
|
|
2089
|
+
const fetchFn = await getFetch();
|
|
2090
|
+
const url = `${apiBase}${endpoint}`;
|
|
2091
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
2092
|
+
fetchFn,
|
|
2093
|
+
url,
|
|
2094
|
+
{
|
|
2095
|
+
method: "POST",
|
|
2096
|
+
headers: {
|
|
2097
|
+
"Content-Type": "application/json",
|
|
2098
|
+
Authorization: `Bearer ${key}`,
|
|
2099
|
+
"X-Idempotency-Key": String(idempotencyKey || "").trim(),
|
|
2100
|
+
...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
|
|
2101
|
+
},
|
|
2102
|
+
body: JSON.stringify(payload),
|
|
2103
|
+
},
|
|
2104
|
+
timeoutMs
|
|
2105
|
+
);
|
|
2106
|
+
|
|
2107
|
+
if (!resp.ok || body.status !== "success") {
|
|
2108
|
+
const msg = body?.message || `Mission archive request failed (${resp.status})`;
|
|
2109
|
+
const err = new Error(msg);
|
|
2110
|
+
err.status = resp.status;
|
|
2111
|
+
throw err;
|
|
2112
|
+
}
|
|
2113
|
+
return body.data || {};
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2106
2116
|
async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
|
|
2107
2117
|
const fetchFn = await getFetch();
|
|
2108
2118
|
const url = `${apiBase}/project-assistant/run-qaqc/${encodeURIComponent(String(batchId || ""))}`;
|
|
@@ -2404,6 +2414,18 @@ async function runMissionStatus(args) {
|
|
|
2404
2414
|
process.exit(1);
|
|
2405
2415
|
}
|
|
2406
2416
|
|
|
2417
|
+
const shouldSync = shouldSyncMissionStateAfterMutation(args);
|
|
2418
|
+
let syncSummary = null;
|
|
2419
|
+
if (shouldSync) {
|
|
2420
|
+
try {
|
|
2421
|
+
syncSummary = await resyncBootstrapAfterMissionMutation({ args, apiBase, key, timeoutMs });
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
console.error("Mission status update succeeded, but bootstrap sync failed:", err?.message || err);
|
|
2424
|
+
console.error("Run `myte bootstrap` to refresh local Command Center state.");
|
|
2425
|
+
process.exit(1);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2407
2429
|
const output = {
|
|
2408
2430
|
project_id: data.project_id || null,
|
|
2409
2431
|
requested_count: data.requested_count || missionIds.length,
|
|
@@ -2413,6 +2435,116 @@ async function runMissionStatus(args) {
|
|
|
2413
2435
|
rejected_count: data.rejected_count || 0,
|
|
2414
2436
|
new_status: data.new_status || newStatus,
|
|
2415
2437
|
missions: Array.isArray(data.missions) ? data.missions : [],
|
|
2438
|
+
sync: syncSummary,
|
|
2439
|
+
};
|
|
2440
|
+
|
|
2441
|
+
if (args.json) {
|
|
2442
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
if (output.project_id) console.log(`Project ID: ${output.project_id}`);
|
|
2447
|
+
console.log(`Target Status: ${output.new_status}`);
|
|
2448
|
+
console.log(`Requested: ${output.requested_count}`);
|
|
2449
|
+
console.log(`Matched: ${output.matched_count}`);
|
|
2450
|
+
console.log(`Updated: ${output.updated_count}`);
|
|
2451
|
+
console.log(`Unchanged: ${output.unchanged_count}`);
|
|
2452
|
+
console.log(`Rejected: ${output.rejected_count}`);
|
|
2453
|
+
for (const mission of output.missions) {
|
|
2454
|
+
const missionId = String(mission?.mission_id || "").trim() || "(unknown)";
|
|
2455
|
+
const status = String(mission?.status || "").trim() || "unknown";
|
|
2456
|
+
const detailParts = [];
|
|
2457
|
+
if (mission?.current_status) detailParts.push(`current=${mission.current_status}`);
|
|
2458
|
+
if (mission?.new_status) detailParts.push(`target=${mission.new_status}`);
|
|
2459
|
+
console.log(`- ${missionId}: ${status}${detailParts.length ? ` (${detailParts.join(", ")})` : ""}`);
|
|
2460
|
+
}
|
|
2461
|
+
if (syncSummary?.output_root) {
|
|
2462
|
+
console.log(`Synced Command Center: ${syncSummary.output_root}`);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
async function runMissionArchiveCommand(args) {
|
|
2467
|
+
const key = getProjectApiKey();
|
|
2468
|
+
const commandLabel = "mission archive";
|
|
2469
|
+
if (!key) {
|
|
2470
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
2471
|
+
process.exit(1);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
const missionIds = parseMissionIdsArg(args);
|
|
2475
|
+
if (!missionIds.length) {
|
|
2476
|
+
console.error(`Missing --mission-ids for \`myte ${commandLabel}\`.`);
|
|
2477
|
+
printHelp();
|
|
2478
|
+
process.exit(1);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
const payload = {
|
|
2482
|
+
mission_ids: missionIds,
|
|
2483
|
+
};
|
|
2484
|
+
const reason = firstNonEmptyString(args.reason);
|
|
2485
|
+
if (reason) payload.reason = reason;
|
|
2486
|
+
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
2487
|
+
if (clientSessionId) payload.client_session_id = clientSessionId;
|
|
2488
|
+
|
|
2489
|
+
const operation = "mission-archive";
|
|
2490
|
+
const idempotencyKey = resolveProjectMutationIdempotencyKey({
|
|
2491
|
+
args,
|
|
2492
|
+
operation,
|
|
2493
|
+
payload,
|
|
2494
|
+
});
|
|
2495
|
+
|
|
2496
|
+
if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
|
|
2497
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
2502
|
+
const apiBase = resolveApiBase(args);
|
|
2503
|
+
const endpoint = "/project-assistant/mission-archive";
|
|
2504
|
+
|
|
2505
|
+
let data;
|
|
2506
|
+
try {
|
|
2507
|
+
data = await createMissionArchiveUpdate({
|
|
2508
|
+
apiBase,
|
|
2509
|
+
key,
|
|
2510
|
+
timeoutMs,
|
|
2511
|
+
endpoint,
|
|
2512
|
+
payload,
|
|
2513
|
+
idempotencyKey,
|
|
2514
|
+
clientSessionId,
|
|
2515
|
+
});
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
if (err?.name === "AbortError") {
|
|
2518
|
+
console.error(`Request timed out after ${timeoutMs}ms`);
|
|
2519
|
+
} else {
|
|
2520
|
+
console.error("Mission archive failed:", err?.message || err);
|
|
2521
|
+
}
|
|
2522
|
+
process.exit(1);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
const shouldSync = shouldSyncMissionStateAfterMutation(args);
|
|
2526
|
+
let syncSummary = null;
|
|
2527
|
+
if (shouldSync) {
|
|
2528
|
+
try {
|
|
2529
|
+
syncSummary = await resyncBootstrapAfterMissionMutation({ args, apiBase, key, timeoutMs });
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
console.error("Mission archive succeeded, but bootstrap sync failed:", err?.message || err);
|
|
2532
|
+
console.error("Run `myte bootstrap` to refresh local Command Center state.");
|
|
2533
|
+
process.exit(1);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
const output = {
|
|
2538
|
+
project_id: data.project_id || null,
|
|
2539
|
+
requested_count: data.requested_count || missionIds.length,
|
|
2540
|
+
matched_count: data.matched_count || 0,
|
|
2541
|
+
updated_count: data.updated_count || 0,
|
|
2542
|
+
unchanged_count: data.unchanged_count || 0,
|
|
2543
|
+
rejected_count: data.rejected_count || 0,
|
|
2544
|
+
archive_state: data.archive_state || "archived",
|
|
2545
|
+
new_status: data.new_status || "Archived",
|
|
2546
|
+
missions: Array.isArray(data.missions) ? data.missions : [],
|
|
2547
|
+
sync: syncSummary,
|
|
2416
2548
|
};
|
|
2417
2549
|
|
|
2418
2550
|
if (args.json) {
|
|
@@ -2421,6 +2553,7 @@ async function runMissionStatus(args) {
|
|
|
2421
2553
|
}
|
|
2422
2554
|
|
|
2423
2555
|
if (output.project_id) console.log(`Project ID: ${output.project_id}`);
|
|
2556
|
+
console.log(`Archive State: ${output.archive_state}`);
|
|
2424
2557
|
console.log(`Target Status: ${output.new_status}`);
|
|
2425
2558
|
console.log(`Requested: ${output.requested_count}`);
|
|
2426
2559
|
console.log(`Matched: ${output.matched_count}`);
|
|
@@ -2435,6 +2568,9 @@ async function runMissionStatus(args) {
|
|
|
2435
2568
|
if (mission?.new_status) detailParts.push(`target=${mission.new_status}`);
|
|
2436
2569
|
console.log(`- ${missionId}: ${status}${detailParts.length ? ` (${detailParts.join(", ")})` : ""}`);
|
|
2437
2570
|
}
|
|
2571
|
+
if (syncSummary?.output_root) {
|
|
2572
|
+
console.log(`Synced Command Center: ${syncSummary.output_root}`);
|
|
2573
|
+
}
|
|
2438
2574
|
}
|
|
2439
2575
|
|
|
2440
2576
|
function formatTargetContacts(contacts) {
|
|
@@ -2722,9 +2858,7 @@ function bootstrapScopedPathId(item, preferredKeys, scopeKeys, fallbackKeys, fal
|
|
|
2722
2858
|
}
|
|
2723
2859
|
|
|
2724
2860
|
function stringifyYaml(value) {
|
|
2725
|
-
|
|
2726
|
-
const YAML = require("yaml");
|
|
2727
|
-
return YAML.stringify(value, { lineWidth: 0 });
|
|
2861
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
2728
2862
|
}
|
|
2729
2863
|
|
|
2730
2864
|
function stableJsonStringify(value) {
|
|
@@ -2741,9 +2875,116 @@ function stableJsonStringify(value) {
|
|
|
2741
2875
|
}
|
|
2742
2876
|
|
|
2743
2877
|
function parseYaml(text) {
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2878
|
+
const raw = String(text || "").trim();
|
|
2879
|
+
if (!raw) return null;
|
|
2880
|
+
if (raw.startsWith("{") || raw.startsWith("[")) {
|
|
2881
|
+
return JSON.parse(raw);
|
|
2882
|
+
}
|
|
2883
|
+
return parseSimpleYaml(raw);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
function parseScalarYaml(value) {
|
|
2887
|
+
const raw = String(value || "").trim();
|
|
2888
|
+
if (raw === "") return "";
|
|
2889
|
+
if (raw === "null" || raw === "~") return null;
|
|
2890
|
+
if (raw === "true") return true;
|
|
2891
|
+
if (raw === "false") return false;
|
|
2892
|
+
if (/^-?\d+(?:\.\d+)?$/.test(raw)) return Number(raw);
|
|
2893
|
+
if (
|
|
2894
|
+
(raw.startsWith('"') && raw.endsWith('"')) ||
|
|
2895
|
+
(raw.startsWith("'") && raw.endsWith("'"))
|
|
2896
|
+
) {
|
|
2897
|
+
return raw.slice(1, -1);
|
|
2898
|
+
}
|
|
2899
|
+
return raw;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
function parseSimpleYaml(text) {
|
|
2903
|
+
const root = {};
|
|
2904
|
+
const stack = [{ indent: -1, type: "object", value: root }];
|
|
2905
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
2906
|
+
let lastScalar = null;
|
|
2907
|
+
|
|
2908
|
+
const nextContentLine = (startIndex) => {
|
|
2909
|
+
for (let i = startIndex; i < lines.length; i += 1) {
|
|
2910
|
+
const raw = lines[i];
|
|
2911
|
+
if (!raw || !raw.trim() || raw.trim().startsWith("#")) continue;
|
|
2912
|
+
return raw;
|
|
2913
|
+
}
|
|
2914
|
+
return "";
|
|
2915
|
+
};
|
|
2916
|
+
|
|
2917
|
+
const containerForEmpty = (lineIndex) => {
|
|
2918
|
+
const next = nextContentLine(lineIndex + 1);
|
|
2919
|
+
return next.trim().startsWith("- ") ? [] : {};
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2922
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
2923
|
+
const rawLine = lines[lineIndex];
|
|
2924
|
+
if (!rawLine || !rawLine.trim() || rawLine.trim().startsWith("#")) continue;
|
|
2925
|
+
const indent = rawLine.match(/^ */)[0].length;
|
|
2926
|
+
const line = rawLine.trim();
|
|
2927
|
+
|
|
2928
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
2929
|
+
stack.pop();
|
|
2930
|
+
}
|
|
2931
|
+
const parent = stack[stack.length - 1].value;
|
|
2932
|
+
|
|
2933
|
+
if (line.startsWith("- ")) {
|
|
2934
|
+
if (!Array.isArray(parent)) {
|
|
2935
|
+
throw new Error("Unsupported YAML list placement.");
|
|
2936
|
+
}
|
|
2937
|
+
const body = line.slice(2).trim();
|
|
2938
|
+
if (!body) {
|
|
2939
|
+
const child = containerForEmpty(lineIndex);
|
|
2940
|
+
parent.push(child);
|
|
2941
|
+
stack.push({ indent, type: Array.isArray(child) ? "array" : "object", value: child });
|
|
2942
|
+
} else if (body.includes(":")) {
|
|
2943
|
+
const [key, ...rest] = body.split(":");
|
|
2944
|
+
const obj = {};
|
|
2945
|
+
const keyName = key.trim();
|
|
2946
|
+
const valueText = rest.join(":").trim();
|
|
2947
|
+
const value = valueText ? parseScalarYaml(valueText) : containerForEmpty(lineIndex);
|
|
2948
|
+
obj[keyName] = value;
|
|
2949
|
+
parent.push(obj);
|
|
2950
|
+
stack.push({ indent, type: "object", value: obj });
|
|
2951
|
+
if (value && typeof value === "object") {
|
|
2952
|
+
lastScalar = null;
|
|
2953
|
+
stack.push({ indent: indent + 2, type: Array.isArray(value) ? "array" : "object", value });
|
|
2954
|
+
} else {
|
|
2955
|
+
lastScalar = { container: obj, key: keyName, indent };
|
|
2956
|
+
}
|
|
2957
|
+
} else {
|
|
2958
|
+
parent.push(parseScalarYaml(body));
|
|
2959
|
+
lastScalar = { container: parent, key: parent.length - 1, indent };
|
|
2960
|
+
}
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
const sep = line.indexOf(":");
|
|
2965
|
+
if (sep === -1) {
|
|
2966
|
+
if (lastScalar && indent > lastScalar.indent && lastScalar.container) {
|
|
2967
|
+
const previous = lastScalar.container[lastScalar.key];
|
|
2968
|
+
if (typeof previous === "string") {
|
|
2969
|
+
lastScalar.container[lastScalar.key] = `${previous} ${parseScalarYaml(line)}`;
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
throw new Error(`Unsupported YAML line: ${line}`);
|
|
2974
|
+
}
|
|
2975
|
+
const key = line.slice(0, sep).trim();
|
|
2976
|
+
const valueText = line.slice(sep + 1).trim();
|
|
2977
|
+
const value = valueText ? parseScalarYaml(valueText) : containerForEmpty(lineIndex);
|
|
2978
|
+
parent[key] = value;
|
|
2979
|
+
if (value && typeof value === "object") {
|
|
2980
|
+
lastScalar = null;
|
|
2981
|
+
stack.push({ indent, type: Array.isArray(value) ? "array" : "object", value });
|
|
2982
|
+
} else {
|
|
2983
|
+
lastScalar = { container: parent, key, indent };
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
return root;
|
|
2747
2988
|
}
|
|
2748
2989
|
|
|
2749
2990
|
function writeYamlFile(filePath, value) {
|
|
@@ -3437,6 +3678,11 @@ function shouldSyncSuggestionsAfterMutation(args) {
|
|
|
3437
3678
|
return !rawArgs.includes("--no-sync");
|
|
3438
3679
|
}
|
|
3439
3680
|
|
|
3681
|
+
function shouldSyncMissionStateAfterMutation(args) {
|
|
3682
|
+
const rawArgs = Array.isArray(args?.__raw) ? args.__raw : [];
|
|
3683
|
+
return !rawArgs.includes("--no-sync");
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3440
3686
|
function resolveBooleanFlag(args, dashedName, defaultValue) {
|
|
3441
3687
|
const rawArgs = Array.isArray(args?.__raw) ? args.__raw : [];
|
|
3442
3688
|
const enabledToken = `--${dashedName}`;
|
|
@@ -3601,6 +3847,34 @@ function normalizeItemsPayload(rawPayload, label) {
|
|
|
3601
3847
|
throw new Error(`${label} payload must be an object, array, or { items: [...] } envelope.`);
|
|
3602
3848
|
}
|
|
3603
3849
|
|
|
3850
|
+
function validateSuggestionsCreatePayload(payload) {
|
|
3851
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
3852
|
+
for (const [index, item] of items.entries()) {
|
|
3853
|
+
if (!isPlainObject(item)) continue;
|
|
3854
|
+
const changeType = String(item.change_type || "").trim().toLowerCase();
|
|
3855
|
+
if (!["update", "create"].includes(changeType)) {
|
|
3856
|
+
throw new Error(`Invalid suggestions create items[${index}].change_type: use update or create.`);
|
|
3857
|
+
}
|
|
3858
|
+
if (changeType === "update" && !String(item.mission_id || "").trim()) {
|
|
3859
|
+
throw new Error(`Invalid suggestions create items[${index}].mission_id: update suggestions require mission_id.`);
|
|
3860
|
+
}
|
|
3861
|
+
if (changeType === "create") {
|
|
3862
|
+
const changeSet = isPlainObject(item.change_set) ? item.change_set : item;
|
|
3863
|
+
if (!String(changeSet.title || "").trim() || !String(changeSet.description || "").trim()) {
|
|
3864
|
+
throw new Error(`Invalid suggestions create items[${index}].change_set: create suggestions require title and description.`);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
function validateSuggestionsRevisePayload(payload) {
|
|
3871
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
3872
|
+
for (const [index, item] of items.entries()) {
|
|
3873
|
+
if (!isPlainObject(item) || item.change_type === undefined) continue;
|
|
3874
|
+
throw new Error(`Invalid suggestions revise items[${index}].change_type: revise keeps the existing thread change_type; provide suggestion_id, change_description/change_set, and expected_revision instead.`);
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3604
3878
|
function filterWorkspaceDraftsByIndex(drafts, indexesToRemove) {
|
|
3605
3879
|
const removal = new Set(Array.from(indexesToRemove || []).map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0));
|
|
3606
3880
|
if (!Array.isArray(drafts) || !removal.size) {
|
|
@@ -4214,6 +4488,8 @@ async function runBootstrap(args) {
|
|
|
4214
4488
|
const wrapperRoot = resolved.root;
|
|
4215
4489
|
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
4216
4490
|
const dryRun = Boolean(args["dry-run"] || args.dryRun);
|
|
4491
|
+
const includeSuggestions = resolveBooleanFlag(args, "suggestions", true);
|
|
4492
|
+
const actorScope = resolveActorScope(args, {});
|
|
4217
4493
|
const summary = {
|
|
4218
4494
|
api_base: apiBase,
|
|
4219
4495
|
project_id: snapshot?.project?.id || null,
|
|
@@ -4234,6 +4510,16 @@ async function runBootstrap(args) {
|
|
|
4234
4510
|
snapshot_hash: snapshot.snapshot_hash || null,
|
|
4235
4511
|
generated_at: snapshot.generated_at || null,
|
|
4236
4512
|
dry_run: dryRun,
|
|
4513
|
+
mission_ops: includeSuggestions
|
|
4514
|
+
? {
|
|
4515
|
+
included: true,
|
|
4516
|
+
actor_scope: actorScope,
|
|
4517
|
+
dry_run: dryRun,
|
|
4518
|
+
}
|
|
4519
|
+
: {
|
|
4520
|
+
included: false,
|
|
4521
|
+
skipped: true,
|
|
4522
|
+
},
|
|
4237
4523
|
};
|
|
4238
4524
|
|
|
4239
4525
|
if (dryRun) {
|
|
@@ -4255,6 +4541,27 @@ async function runBootstrap(args) {
|
|
|
4255
4541
|
const writeResult = writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir });
|
|
4256
4542
|
summary.data_root = writeResult.dataRoot;
|
|
4257
4543
|
|
|
4544
|
+
if (includeSuggestions) {
|
|
4545
|
+
let missionOpsSnapshot;
|
|
4546
|
+
try {
|
|
4547
|
+
missionOpsSnapshot = await fetchSuggestionsSyncSnapshot({ apiBase, key, timeoutMs, actorScope });
|
|
4548
|
+
} catch (err) {
|
|
4549
|
+
console.error("Failed to fetch mission suggestions during bootstrap:", err?.message || err);
|
|
4550
|
+
console.error("Run with --no-suggestions only if you intentionally want mission cards without mission review-thread state.");
|
|
4551
|
+
process.exit(1);
|
|
4552
|
+
}
|
|
4553
|
+
const missionOpsWrite = writeMissionOpsSnapshot({ snapshot: missionOpsSnapshot, wrapperRoot, outputDir });
|
|
4554
|
+
const missionOpsCounts = missionOpsWrite?.manifest?.sync?.counts || {};
|
|
4555
|
+
summary.mission_ops = {
|
|
4556
|
+
included: true,
|
|
4557
|
+
actor_scope: missionOpsWrite?.manifest?.sync?.actor_scope || actorScope,
|
|
4558
|
+
total_threads: Number(missionOpsCounts.total_threads || 0),
|
|
4559
|
+
actionable_threads: Number(missionOpsCounts.actionable_threads || 0),
|
|
4560
|
+
snapshot_hash: missionOpsWrite?.manifest?.snapshot_hash || null,
|
|
4561
|
+
data_root: missionOpsWrite?.dataRoot || writeResult.dataRoot,
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4258
4565
|
if (args.json) {
|
|
4259
4566
|
console.log(JSON.stringify(summary, null, 2));
|
|
4260
4567
|
return;
|
|
@@ -4267,6 +4574,11 @@ async function runBootstrap(args) {
|
|
|
4267
4574
|
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
4268
4575
|
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
4269
4576
|
console.log(`Wrote: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
|
|
4577
|
+
if (summary.mission_ops?.included) {
|
|
4578
|
+
console.log(`Wrote mission ops: total_threads=${summary.mission_ops.total_threads}, actionable_threads=${summary.mission_ops.actionable_threads}`);
|
|
4579
|
+
} else {
|
|
4580
|
+
console.log("Skipped mission ops sync.");
|
|
4581
|
+
}
|
|
4270
4582
|
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
4271
4583
|
}
|
|
4272
4584
|
|
|
@@ -5315,6 +5627,11 @@ async function buildSuggestionsMutationContext(args, mode) {
|
|
|
5315
5627
|
if (clientSessionId && !payload.client_session_id) {
|
|
5316
5628
|
payload.client_session_id = clientSessionId;
|
|
5317
5629
|
}
|
|
5630
|
+
if (mode === "create") {
|
|
5631
|
+
validateSuggestionsCreatePayload(payload);
|
|
5632
|
+
} else if (mode === "revise") {
|
|
5633
|
+
validateSuggestionsRevisePayload(payload);
|
|
5634
|
+
}
|
|
5318
5635
|
const idempotencyKey = resolveSuggestionsIdempotencyKey({
|
|
5319
5636
|
args,
|
|
5320
5637
|
mode,
|
|
@@ -5353,6 +5670,42 @@ async function resyncMissionOpsAfterMutation({ apiBase, key, timeoutMs, actorSco
|
|
|
5353
5670
|
};
|
|
5354
5671
|
}
|
|
5355
5672
|
|
|
5673
|
+
async function resyncBootstrapAfterMissionMutation({ args, apiBase, key, timeoutMs }) {
|
|
5674
|
+
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
5675
|
+
const existingPaths = resolveMissionOpsPaths(args, { requireExisting: false });
|
|
5676
|
+
const existingPayload = existingPaths && fs.existsSync(existingPaths.missionOpsPath)
|
|
5677
|
+
? (readYamlFile(existingPaths.missionOpsPath) || {})
|
|
5678
|
+
: {};
|
|
5679
|
+
const actorScope = resolveActorScope(args, existingPayload);
|
|
5680
|
+
const snapshot = await fetchBootstrapSnapshot({ apiBase, key, timeoutMs });
|
|
5681
|
+
const resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
5682
|
+
const wrapperRoot = !outputDir && existingPaths?.wrapperRoot ? existingPaths.wrapperRoot : resolved.root;
|
|
5683
|
+
const writeResult = writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir });
|
|
5684
|
+
const missionOpsSnapshot = await fetchSuggestionsSyncSnapshot({ apiBase, key, timeoutMs, actorScope });
|
|
5685
|
+
const missionOpsWrite = writeMissionOpsSnapshot({ snapshot: missionOpsSnapshot, wrapperRoot, outputDir });
|
|
5686
|
+
const missionOpsCounts = missionOpsWrite?.manifest?.sync?.counts || {};
|
|
5687
|
+
const outputRoot = outputDir ? path.resolve(process.cwd(), String(outputDir)) : path.join(wrapperRoot, "MyteCommandCenter");
|
|
5688
|
+
return {
|
|
5689
|
+
wrapper_root: wrapperRoot,
|
|
5690
|
+
output_root: outputRoot,
|
|
5691
|
+
data_root: writeResult.dataRoot,
|
|
5692
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
5693
|
+
counts: {
|
|
5694
|
+
phases: Array.isArray(snapshot.phases) ? snapshot.phases.length : 0,
|
|
5695
|
+
epics: Array.isArray(snapshot.epics) ? snapshot.epics.length : 0,
|
|
5696
|
+
stories: Array.isArray(snapshot.stories) ? snapshot.stories.length : 0,
|
|
5697
|
+
missions: Array.isArray(snapshot.missions) ? snapshot.missions.length : 0,
|
|
5698
|
+
},
|
|
5699
|
+
mission_ops: {
|
|
5700
|
+
actor_scope: missionOpsWrite?.manifest?.sync?.actor_scope || actorScope,
|
|
5701
|
+
total_threads: Number(missionOpsCounts.total_threads || 0),
|
|
5702
|
+
actionable_threads: Number(missionOpsCounts.actionable_threads || 0),
|
|
5703
|
+
snapshot_hash: missionOpsWrite?.manifest?.snapshot_hash || null,
|
|
5704
|
+
data_root: missionOpsWrite?.dataRoot || writeResult.dataRoot,
|
|
5705
|
+
},
|
|
5706
|
+
};
|
|
5707
|
+
}
|
|
5708
|
+
|
|
5356
5709
|
async function runSuggestionsCreate(args) {
|
|
5357
5710
|
let context;
|
|
5358
5711
|
try {
|
|
@@ -5920,7 +6273,7 @@ async function main() {
|
|
|
5920
6273
|
process.exit(1);
|
|
5921
6274
|
}
|
|
5922
6275
|
if (command === "mission") {
|
|
5923
|
-
console.error("Unknown mission command. Use `myte mission status
|
|
6276
|
+
console.error("Unknown mission command. Use `myte mission status` or `myte mission archive`.");
|
|
5924
6277
|
process.exit(1);
|
|
5925
6278
|
}
|
|
5926
6279
|
const args = parseArgs(rest);
|
|
@@ -5954,6 +6307,11 @@ async function main() {
|
|
|
5954
6307
|
return;
|
|
5955
6308
|
}
|
|
5956
6309
|
|
|
6310
|
+
if (command === "mission-archive") {
|
|
6311
|
+
await runMissionArchiveCommand(args);
|
|
6312
|
+
return;
|
|
6313
|
+
}
|
|
6314
|
+
|
|
5957
6315
|
if (command === "sync-qaqc" || command === "qaqc-sync") {
|
|
5958
6316
|
await runSyncQaqc(args);
|
|
5959
6317
|
return;
|