@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/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
- try {
50
- // eslint-disable-next-line global-require
51
- const dotenv = require("dotenv");
52
- dotenv.config(envPath ? { path: envPath } : undefined);
53
- return;
54
- } catch (err) {
55
- if (!envPath || !fs.existsSync(envPath)) return;
56
- const content = fs.readFileSync(envPath, "utf8");
57
- content.split(/\r?\n/).forEach((line) => {
58
- if (!line || line.trim().startsWith("#")) return;
59
- const idx = line.indexOf("=");
60
- if (idx === -1) return;
61
- const key = line.slice(0, idx).trim();
62
- const val = line.slice(idx + 1).trim();
63
- if (key && !(key in process.env)) {
64
- process.env[key] = val;
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
- try {
117
- // eslint-disable-next-line global-require
118
- const parsed = require("minimist")(argv, {
119
- boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json", "stdin", "with-prd-text", "wait", "sync", "force", "json-response"],
120
- string: [
121
- "query",
122
- "q",
123
- "payload-file",
124
- "context",
125
- "ctx",
126
- "file",
127
- "actor-scope",
128
- "base-url",
129
- "timeout-ms",
130
- "diff-limit",
131
- "max-output-tokens",
132
- "max-tokens",
133
- "temperature",
134
- "title",
135
- "description",
136
- "feedback-text",
137
- "output-dir",
138
- "content",
139
- "subject",
140
- "body-markdown",
141
- "body-file",
142
- "target-contact-id",
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
- if (token.startsWith("--")) {
191
- const key = token.slice(2);
192
- const next = argv[i + 1];
193
- if (next && !next.startsWith("-")) {
194
- parsed[key] = next;
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
- parsed._.push(token);
158
+ setValue(key, true);
210
159
  }
160
+ continue;
211
161
  }
212
- parsed.__raw = Array.isArray(argv) ? [...argv] : [];
213
- return parsed;
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
- " - Each item carries the full PRD markdown blob that becomes the stored PRD document and generated DOCX source",
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 status (quote multi-id values on PowerShell)",
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 mission-ops resync after suggestions create/revise/review",
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
- const mod = await import("node-fetch");
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
- // eslint-disable-next-line global-require
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
- // eslint-disable-next-line global-require
2745
- const YAML = require("yaml");
2746
- return YAML.parse(String(text || ""));
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 --mission-ids \"M001,M002\" --status todo|in_progress|done`.");
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;