@mytegroupinc/myte-core 0.0.5 → 0.0.7

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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/cli.js +566 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,12 +4,21 @@ Internal implementation package for the `myte` CLI.
4
4
 
5
5
  Most users should install the unscoped wrapper instead:
6
6
  - `npm install myte` then `npx myte bootstrap`
7
+ - `npm install myte` then `npx myte sync-qaqc`
7
8
  - `npm install myte` then `npx myte query "..." --with-diff`
8
9
  - `npm install myte` then `npm exec myte -- query "..." --with-diff`
10
+ - `npm install myte` then `npx myte update-team "Backend deploy completed; QAQC rerun queued."`
11
+ - `npm install myte` then `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
9
12
  - `npm i -g myte` then `myte bootstrap`
13
+ - `npm i -g myte` then `myte sync-qaqc`
10
14
  - `npm i -g myte` then `myte query "..." --with-diff`
15
+ - `npm i -g myte` then `myte update-team "Backend deploy completed; QAQC rerun queued."`
16
+ - `npm i -g myte` then `myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
11
17
  - `npx myte@latest bootstrap`
18
+ - `npx myte@latest sync-qaqc`
12
19
  - `npx myte@latest query "..." --with-diff`
20
+ - `npx myte@latest update-team "Backend deploy completed; QAQC rerun queued."`
21
+ - `npx myte@latest update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
13
22
  - `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
14
23
  - `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
15
24
 
@@ -26,7 +35,17 @@ Notes:
26
35
  - `bootstrap` is a local file materialization path, not a hosted file download.
27
36
  - `bootstrap` expects to run from a wrapper root that contains the project's configured repo folders.
28
37
  - `bootstrap` writes `MyteCommandCenter/data/phases`, `epics`, `stories`, `missions`, `project.yml`, and `bootstrap-manifest.json`.
38
+ - `bootstrap` materializes a public Command Center DTO, not raw backend documents.
39
+ - `bootstrap` excludes internal keys like `_id`, `org_id`, `project_id`, `created_by`, `assigned_to`, and raw `qa_qc_results`.
40
+ - `sync-qaqc` works without `bootstrap`; it creates `MyteCommandCenter/data/qaqc` automatically if missing.
41
+ - `sync-qaqc` writes active mission QAQC cards to `MyteCommandCenter/data/qaqc/active-missions` and refreshes matching `MyteCommandCenter/data/missions` cards.
42
+ - `sync-qaqc` only exports active `Todo` / `In Progress` missions plus a public QAQC summary and sanitized latest batch metadata.
43
+ - `sync-qaqc` removes previously QAQC-managed mission files from `MyteCommandCenter/data/missions` once they leave the active set.
29
44
  - `create-prd` is a deterministic PRD upload path, not an LLM generation command.
45
+ - `update-team` creates a project comment through `/api/project-assistant/project-comment`.
46
+ - `update-client` creates a client update draft through `/api/project-assistant/client-update-drafts`.
47
+ - `update-client` requires `--subject` plus body markdown from `--body-markdown`, `--body-file`, positional input, or `--stdin`.
48
+ - `update-client` accepts optional `--target-contact-id` repeats or `--target-contact-ids <id1,id2>`.
30
49
  - `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
31
50
  - `--with-diff` includes per-repo diagnostics in `print-context` payload:
32
51
  - missing repo directories
@@ -44,8 +63,12 @@ Deterministic `create-prd` contract:
44
63
 
45
64
  Examples:
46
65
  - `npx myte bootstrap`
66
+ - `npx myte sync-qaqc`
47
67
  - `npx myte bootstrap --dry-run --json`
68
+ - `npx myte sync-qaqc --dry-run --json`
48
69
  - `npx myte create-prd ./drafts/auth-prd.md --description "Short card summary"`
49
70
  - `npx myte create-prd ./drafts/auth-prd.md --print-context`
71
+ - `npx myte update-team "Backend deploy completed; QAQC rerun queued."`
72
+ - `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
50
73
 
51
74
  This package is published under the org scope for governance; the public `myte` wrapper delegates here.
package/cli.js CHANGED
@@ -49,7 +49,23 @@ function loadEnv() {
49
49
  }
50
50
 
51
51
  function splitCommand(argv) {
52
- const known = new Set(["query", "ask", "chat", "config", "bootstrap", "create-prd", "add-prd", "prd", "help", "--help", "-h"]);
52
+ const known = new Set([
53
+ "query",
54
+ "ask",
55
+ "chat",
56
+ "config",
57
+ "bootstrap",
58
+ "sync-qaqc",
59
+ "qaqc-sync",
60
+ "create-prd",
61
+ "add-prd",
62
+ "prd",
63
+ "update-team",
64
+ "update-client",
65
+ "help",
66
+ "--help",
67
+ "-h",
68
+ ]);
53
69
  const first = argv[0];
54
70
  if (first && known.has(first)) {
55
71
  const cmd = first === "--help" || first === "-h" ? "help" : first;
@@ -63,7 +79,25 @@ function parseArgs(argv) {
63
79
  // eslint-disable-next-line global-require
64
80
  return require("minimist")(argv, {
65
81
  boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json", "stdin"],
66
- string: ["query", "q", "context", "ctx", "base-url", "timeout-ms", "diff-limit", "title", "description", "feedback-text", "output-dir"],
82
+ string: [
83
+ "query",
84
+ "q",
85
+ "context",
86
+ "ctx",
87
+ "base-url",
88
+ "timeout-ms",
89
+ "diff-limit",
90
+ "title",
91
+ "description",
92
+ "feedback-text",
93
+ "output-dir",
94
+ "content",
95
+ "subject",
96
+ "body-markdown",
97
+ "body-file",
98
+ "target-contact-id",
99
+ "target-contact-ids",
100
+ ],
67
101
  alias: {
68
102
  q: "query",
69
103
  d: "with-diff",
@@ -117,10 +151,14 @@ function printHelp() {
117
151
  " myte query \"<text>\" [--with-diff] [--context \"...\"]",
118
152
  " myte config [--json]",
119
153
  " myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
154
+ " myte sync-qaqc [--output-dir ./MyteCommandCenter] [--json]",
155
+ " myte update-team \"<content>\" [--json]",
156
+ " myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] [--json]",
120
157
  " myte chat",
121
158
  " myte create-prd <file.md> [--json] [--title \"...\"] [--description \"...\"]",
122
159
  " myte add-prd <file.md> [--json]",
123
160
  " cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
161
+ " cat update.md | myte update-client --stdin --subject \"Weekly client update\"",
124
162
  "",
125
163
  "Run forms:",
126
164
  " npm install myte then npx myte query \"...\" --with-diff",
@@ -136,21 +174,42 @@ function printHelp() {
136
174
  " - Writes MyteCommandCenter/data/phases, epics, stories, and missions locally",
137
175
  " - Uses the project-scoped bootstrap snapshot from the Myte API",
138
176
  "",
177
+ "sync-qaqc contract:",
178
+ " - Run from the wrapper root that contains the project's configured repo folders",
179
+ " - Works even if bootstrap has not been run yet; it creates MyteCommandCenter/data/qaqc automatically",
180
+ " - Writes active mission QAQC context under MyteCommandCenter/data/qaqc/active-missions",
181
+ " - Refreshes matching MyteCommandCenter/data/missions cards for active missions only",
182
+ "",
139
183
  "create-prd contract:",
140
184
  " - Required: valid MYTE_API_KEY, PRD markdown body, title",
141
185
  " - Title source: myte-kanban.title, first # heading, or --title",
142
186
  " - Description source: myte-kanban.description or --description",
143
187
  " - PRD DOCX content: the markdown body is stored verbatim",
144
188
  "",
189
+ "update-team contract:",
190
+ " - Creates a project team comment through /api/project-assistant/project-comment",
191
+ " - Required: content (inline, --content, or --stdin)",
192
+ "",
193
+ "update-client contract:",
194
+ " - Creates a client update draft through /api/project-assistant/client-update-drafts",
195
+ " - Required: --subject and body markdown (via --body-markdown, --body-file, positional file/text, or --stdin)",
196
+ " - Optional: --target-contact-id <id> (repeatable) or --target-contact-ids <id1,id2>",
197
+ "",
145
198
  "Options:",
146
199
  " --with-diff Include deterministic git diffs (project-scoped)",
147
200
  " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
148
201
  " --timeout-ms <ms> Request timeout (default: 300000)",
149
202
  " --base-url <url> API base (default: https://api.myte.dev)",
150
203
  " --output-dir <path> Bootstrap output directory (default: <wrapper-root>/MyteCommandCenter)",
151
- " --stdin Read PRD content from stdin instead of a file path",
204
+ " --stdin Read supported command content from stdin instead of inline text or a file path",
152
205
  " --title <text> Override PRD title for raw markdown uploads",
153
206
  " --description <text> Set feedback description/card summary for raw markdown uploads",
207
+ " --content <text> Team update content for update-team",
208
+ " --subject <text> Client update subject for update-client",
209
+ " --body-markdown <md> Client update markdown body for update-client",
210
+ " --body-file <path> Read client update markdown body from a file",
211
+ " --target-contact-id Add one client contact ObjectId (repeatable)",
212
+ " --target-contact-ids Comma-separated client contact ObjectIds",
154
213
  " --print-context Print JSON payload and exit (no query call)",
155
214
  " --no-fetch Don't git fetch origin main/master before diff",
156
215
  "",
@@ -158,6 +217,10 @@ function printHelp() {
158
217
  " myte query \"What changed in logging?\" --with-diff",
159
218
  " myte bootstrap",
160
219
  " myte bootstrap --output-dir ./MyteCommandCenter",
220
+ " myte sync-qaqc",
221
+ " myte update-team \"Backend deploy completed; QAQC rerun queued.\"",
222
+ " myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md",
223
+ " myte update-client --subject \"Weekly client update\" --body-markdown \"## Progress\\n- Login complete\" --target-contact-ids 507f1f77bcf86cd799439011,507f1f77bcf86cd799439012",
161
224
  " myte create-prd ./drafts/auth-prd.md --description \"Short card summary\"",
162
225
  " cat ./drafts/auth-prd.md | myte create-prd --stdin",
163
226
  " myte config",
@@ -194,6 +257,139 @@ async function readStdinText() {
194
257
  });
195
258
  }
196
259
 
260
+ function firstNonEmptyString(...values) {
261
+ for (const value of values) {
262
+ if (value === undefined || value === null) continue;
263
+ if (Array.isArray(value)) {
264
+ const nested = firstNonEmptyString(...value);
265
+ if (nested) return nested;
266
+ continue;
267
+ }
268
+ const text = String(value).trim();
269
+ if (text) return text;
270
+ }
271
+ return "";
272
+ }
273
+
274
+ function toStringArray(value) {
275
+ if (value === undefined || value === null) return [];
276
+ return Array.isArray(value) ? value.flatMap((item) => toStringArray(item)) : [String(value)];
277
+ }
278
+
279
+ function parseCsvValues(...values) {
280
+ return values
281
+ .flatMap((value) => toStringArray(value))
282
+ .flatMap((value) => value.split(","))
283
+ .map((value) => value.trim())
284
+ .filter(Boolean);
285
+ }
286
+
287
+ function dedupeStrings(values) {
288
+ const seen = new Set();
289
+ const unique = [];
290
+ for (const value of values) {
291
+ const normalized = String(value || "").trim();
292
+ if (!normalized || seen.has(normalized)) continue;
293
+ seen.add(normalized);
294
+ unique.push(normalized);
295
+ }
296
+ return unique;
297
+ }
298
+
299
+ function resolveTimeoutMs(args) {
300
+ const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
301
+ const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
302
+ return Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
303
+ }
304
+
305
+ function resolveApiBase(args) {
306
+ const baseRaw =
307
+ args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
308
+ return normalizeApiBase(baseRaw);
309
+ }
310
+
311
+ function getProjectApiKey() {
312
+ return (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
313
+ }
314
+
315
+ function resolveInputFile(candidatePath, label) {
316
+ const resolved = String(candidatePath || "").trim();
317
+ if (!resolved) return null;
318
+ const absPath = path.resolve(process.cwd(), resolved);
319
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
320
+ console.error(`${label} file not found: ${absPath}`);
321
+ process.exit(1);
322
+ }
323
+ return absPath;
324
+ }
325
+
326
+ async function resolveTeamUpdateContent(args) {
327
+ const inlineContent = firstNonEmptyString(args.content, Array.isArray(args._) ? args._.join(" ") : args._);
328
+ const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !inlineContent));
329
+ if (useStdin) {
330
+ const stdinContent = String((await readStdinText()) || "").trim();
331
+ if (!stdinContent) {
332
+ console.error("Team update content is empty.");
333
+ process.exit(1);
334
+ }
335
+ return stdinContent;
336
+ }
337
+
338
+ const content = String(inlineContent || "").trim();
339
+ if (!content) {
340
+ console.error("Missing team update content.");
341
+ printHelp();
342
+ process.exit(1);
343
+ }
344
+ return content;
345
+ }
346
+
347
+ async function resolveClientUpdateBody(args) {
348
+ const inlineBody = firstNonEmptyString(
349
+ args["body-markdown"],
350
+ args.bodyMarkdown,
351
+ args.body_markdown
352
+ );
353
+ if (inlineBody) return inlineBody;
354
+
355
+ const explicitFile = firstNonEmptyString(args["body-file"], args.bodyFile, args.body_file);
356
+ const positionalInput = firstNonEmptyString(Array.isArray(args._) ? args._.join(" ") : args._);
357
+ const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !explicitFile && !positionalInput));
358
+ if (useStdin) {
359
+ const stdinBody = String((await readStdinText()) || "").trim();
360
+ if (!stdinBody) {
361
+ console.error("Client update body_markdown is empty.");
362
+ process.exit(1);
363
+ }
364
+ return stdinBody;
365
+ }
366
+
367
+ const fileCandidate = explicitFile || (positionalInput && fs.existsSync(path.resolve(process.cwd(), positionalInput)) ? positionalInput : "");
368
+ if (fileCandidate) {
369
+ const absPath = resolveInputFile(fileCandidate, "Client update body");
370
+ return String(fs.readFileSync(absPath, "utf8") || "").trim();
371
+ }
372
+
373
+ if (positionalInput) return positionalInput;
374
+
375
+ console.error("Missing client update body_markdown.");
376
+ printHelp();
377
+ process.exit(1);
378
+ }
379
+
380
+ function resolveTargetContactIds(args) {
381
+ return dedupeStrings(
382
+ parseCsvValues(
383
+ args["target-contact-id"],
384
+ args.targetContactId,
385
+ args.target_contact_id,
386
+ args["target-contact-ids"],
387
+ args.targetContactIds,
388
+ args.target_contact_ids
389
+ )
390
+ );
391
+ }
392
+
197
393
  async function getFetch() {
198
394
  if (typeof fetch !== "undefined") return fetch;
199
395
  const mod = await import("node-fetch");
@@ -733,6 +929,28 @@ async function fetchBootstrapSnapshot({ apiBase, key, timeoutMs }) {
733
929
  return body.data || {};
734
930
  }
735
931
 
932
+ async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
933
+ const fetchFn = await getFetch();
934
+ const url = `${apiBase}/project-assistant/qaqc-sync`;
935
+ const { resp, body } = await fetchJsonWithTimeout(
936
+ fetchFn,
937
+ url,
938
+ {
939
+ method: "GET",
940
+ headers: { Authorization: `Bearer ${key}` },
941
+ },
942
+ timeoutMs
943
+ );
944
+
945
+ if (!resp.ok || body.status !== "success") {
946
+ const msg = body?.message || `QAQC sync request failed (${resp.status})`;
947
+ const err = new Error(msg);
948
+ err.status = resp.status;
949
+ throw err;
950
+ }
951
+ return body.data || {};
952
+ }
953
+
736
954
  async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
737
955
  const fetchFn = await getFetch();
738
956
  const url = `${apiBase}${endpoint}`;
@@ -759,6 +977,138 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint =
759
977
  return body.data || {};
760
978
  }
761
979
 
980
+ function formatTargetContacts(contacts) {
981
+ const items = Array.isArray(contacts) ? contacts : [];
982
+ const formatted = items
983
+ .map((contact) => {
984
+ const name = String(contact?.name || "").trim();
985
+ const email = String(contact?.email || "").trim();
986
+ if (name && email) return `${name} <${email}>`;
987
+ return name || email || String(contact?.contact_id || "").trim();
988
+ })
989
+ .filter(Boolean);
990
+ return formatted.join(", ");
991
+ }
992
+
993
+ async function runUpdateTeam(args) {
994
+ const key = getProjectApiKey();
995
+ if (!key) {
996
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
997
+ process.exit(1);
998
+ }
999
+
1000
+ const payload = {
1001
+ content: await resolveTeamUpdateContent(args),
1002
+ };
1003
+
1004
+ if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
1005
+ console.log(JSON.stringify(payload, null, 2));
1006
+ return;
1007
+ }
1008
+
1009
+ const timeoutMs = resolveTimeoutMs(args);
1010
+ const apiBase = resolveApiBase(args);
1011
+
1012
+ let data;
1013
+ try {
1014
+ data = await callAssistantQuery({
1015
+ apiBase,
1016
+ key,
1017
+ payload,
1018
+ timeoutMs,
1019
+ endpoint: "/project-assistant/project-comment",
1020
+ });
1021
+ } catch (err) {
1022
+ if (err?.name === "AbortError") {
1023
+ console.error(`Request timed out after ${timeoutMs}ms`);
1024
+ } else {
1025
+ console.error("Team update failed:", err?.message || err);
1026
+ }
1027
+ process.exit(1);
1028
+ }
1029
+
1030
+ if (args.json) {
1031
+ console.log(JSON.stringify(data, null, 2));
1032
+ return;
1033
+ }
1034
+
1035
+ if (data.comment_id) console.log(`Comment ID: ${data.comment_id}`);
1036
+ if (data.project_id) console.log(`Project ID: ${data.project_id}`);
1037
+ if (data.user?.name) console.log(`Author: ${data.user.name}`);
1038
+ if (data.created_at) console.log(`Created At: ${data.created_at}`);
1039
+ if (data.content) {
1040
+ console.log("Content:");
1041
+ console.log(data.content);
1042
+ }
1043
+ }
1044
+
1045
+ async function runUpdateClient(args) {
1046
+ const key = getProjectApiKey();
1047
+ if (!key) {
1048
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
1049
+ process.exit(1);
1050
+ }
1051
+
1052
+ const subject = firstNonEmptyString(args.subject);
1053
+ if (!subject) {
1054
+ console.error("Missing --subject for client update.");
1055
+ printHelp();
1056
+ process.exit(1);
1057
+ }
1058
+
1059
+ const payload = {
1060
+ subject,
1061
+ body_markdown: await resolveClientUpdateBody(args),
1062
+ target_contact_ids: resolveTargetContactIds(args),
1063
+ };
1064
+
1065
+ if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
1066
+ console.log(JSON.stringify(payload, null, 2));
1067
+ return;
1068
+ }
1069
+
1070
+ const timeoutMs = resolveTimeoutMs(args);
1071
+ const apiBase = resolveApiBase(args);
1072
+
1073
+ let data;
1074
+ try {
1075
+ data = await callAssistantQuery({
1076
+ apiBase,
1077
+ key,
1078
+ payload,
1079
+ timeoutMs,
1080
+ endpoint: "/project-assistant/client-update-drafts",
1081
+ });
1082
+ } catch (err) {
1083
+ if (err?.name === "AbortError") {
1084
+ console.error(`Request timed out after ${timeoutMs}ms`);
1085
+ } else {
1086
+ console.error("Client update draft failed:", err?.message || err);
1087
+ }
1088
+ process.exit(1);
1089
+ }
1090
+
1091
+ if (args.json) {
1092
+ console.log(JSON.stringify(data, null, 2));
1093
+ return;
1094
+ }
1095
+
1096
+ if (data.draft_id) console.log(`Draft ID: ${data.draft_id}`);
1097
+ if (data.project_title) console.log(`Project: ${data.project_title}`);
1098
+ if (data.project_id) console.log(`Project ID: ${data.project_id}`);
1099
+ if (data.status) console.log(`Status: ${data.status}`);
1100
+ if (data.subject) console.log(`Subject: ${data.subject}`);
1101
+ if (data.created_by_name) console.log(`Author: ${data.created_by_name}`);
1102
+ const targets = formatTargetContacts(data.target_contacts);
1103
+ if (targets) console.log(`Targets: ${targets}`);
1104
+ if (data.created_at) console.log(`Created At: ${data.created_at}`);
1105
+ if (data.updated_at) console.log(`Updated At: ${data.updated_at}`);
1106
+ if (data.snippet) {
1107
+ console.log("Snippet:");
1108
+ console.log(data.snippet);
1109
+ }
1110
+ }
1111
+
762
1112
  function ensureDir(dirPath) {
763
1113
  fs.mkdirSync(dirPath, { recursive: true });
764
1114
  }
@@ -799,6 +1149,46 @@ function writeJsonFile(filePath, value) {
799
1149
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
800
1150
  }
801
1151
 
1152
+ function readJsonFile(filePath) {
1153
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
1154
+ try {
1155
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
1156
+ } catch {
1157
+ return null;
1158
+ }
1159
+ }
1160
+
1161
+ const BOOTSTRAP_FORBIDDEN_KEYS = new Set([
1162
+ "_id",
1163
+ "org_id",
1164
+ "project_id",
1165
+ "created_by",
1166
+ "assigned_to",
1167
+ "user_id",
1168
+ "qa_qc_results",
1169
+ "job_id",
1170
+ "job_ids",
1171
+ "celery_task_id",
1172
+ "conversation_id",
1173
+ "error",
1174
+ ]);
1175
+
1176
+ function scrubBootstrapValue(value) {
1177
+ if (Array.isArray(value)) {
1178
+ return value.map((item) => scrubBootstrapValue(item));
1179
+ }
1180
+ if (!value || typeof value !== "object") {
1181
+ return value;
1182
+ }
1183
+
1184
+ const cleaned = {};
1185
+ for (const [key, child] of Object.entries(value)) {
1186
+ if (BOOTSTRAP_FORBIDDEN_KEYS.has(key)) continue;
1187
+ cleaned[key] = scrubBootstrapValue(child);
1188
+ }
1189
+ return cleaned;
1190
+ }
1191
+
802
1192
  function resolveBootstrapWorkspace(repoNames) {
803
1193
  const resolved = resolveConfiguredRepos(repoNames);
804
1194
  if (!resolved.root || !Array.isArray(resolved.repos) || !resolved.repos.length) {
@@ -826,10 +1216,10 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
826
1216
  clearYamlDirectory(storiesDir);
827
1217
  clearYamlDirectory(missionsDir);
828
1218
 
829
- const phases = Array.isArray(snapshot.phases) ? snapshot.phases : [];
830
- const epics = Array.isArray(snapshot.epics) ? snapshot.epics : [];
831
- const stories = Array.isArray(snapshot.stories) ? snapshot.stories : [];
832
- const missions = Array.isArray(snapshot.missions) ? snapshot.missions : [];
1219
+ const phases = Array.isArray(snapshot.phases) ? snapshot.phases.map((item) => scrubBootstrapValue(item)) : [];
1220
+ const epics = Array.isArray(snapshot.epics) ? snapshot.epics.map((item) => scrubBootstrapValue(item)) : [];
1221
+ const stories = Array.isArray(snapshot.stories) ? snapshot.stories.map((item) => scrubBootstrapValue(item)) : [];
1222
+ const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
833
1223
 
834
1224
  phases.forEach((phase, index) => {
835
1225
  const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
@@ -844,19 +1234,19 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
844
1234
  writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
845
1235
  });
846
1236
  missions.forEach((mission, index) => {
847
- const missionId = stableItemId(mission, ["mission_id", "id", "_id"], `M${String(index + 1).padStart(3, "0")}`);
1237
+ const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
848
1238
  writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
849
1239
  });
850
1240
 
851
1241
  if (snapshot.project && typeof snapshot.project === "object") {
852
- writeYamlFile(path.join(dataRoot, "project.yml"), snapshot.project);
1242
+ writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
853
1243
  }
854
1244
 
855
1245
  const manifest = {
856
1246
  schema_version: snapshot.schema_version || 1,
857
1247
  generated_at: snapshot.generated_at || null,
858
1248
  snapshot_hash: snapshot.snapshot_hash || null,
859
- project: snapshot.project || null,
1249
+ project: snapshot.project ? scrubBootstrapValue(snapshot.project) : null,
860
1250
  repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
861
1251
  counts: {
862
1252
  phases: phases.length,
@@ -874,6 +1264,66 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
874
1264
  };
875
1265
  }
876
1266
 
1267
+ function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
1268
+ const targetRoot = outputDir
1269
+ ? path.resolve(process.cwd(), String(outputDir))
1270
+ : path.join(wrapperRoot, "MyteCommandCenter");
1271
+ const dataRoot = path.join(targetRoot, "data");
1272
+ const missionsDir = path.join(dataRoot, "missions");
1273
+ const qaqcRoot = path.join(dataRoot, "qaqc");
1274
+ const activeMissionsDir = path.join(qaqcRoot, "active-missions");
1275
+ const manifestPath = path.join(qaqcRoot, "manifest.json");
1276
+
1277
+ ensureDir(dataRoot);
1278
+ ensureDir(missionsDir);
1279
+ ensureDir(qaqcRoot);
1280
+ const previousManifest = readJsonFile(manifestPath);
1281
+ const previousMissionIds = Array.isArray(previousManifest?.active_mission_ids)
1282
+ ? previousManifest.active_mission_ids.map((item) => String(item).trim()).filter(Boolean)
1283
+ : [];
1284
+ clearYamlDirectory(activeMissionsDir);
1285
+
1286
+ const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
1287
+ const currentMissionIds = [];
1288
+
1289
+ if (snapshot.project && typeof snapshot.project === "object") {
1290
+ writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
1291
+ }
1292
+
1293
+ missions.forEach((mission, index) => {
1294
+ const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
1295
+ currentMissionIds.push(missionId);
1296
+ writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
1297
+ writeYamlFile(path.join(activeMissionsDir, `${missionId}.yml`), mission);
1298
+ });
1299
+
1300
+ const currentMissionIdSet = new Set(currentMissionIds);
1301
+ previousMissionIds.forEach((missionId) => {
1302
+ if (!missionId || currentMissionIdSet.has(missionId)) return;
1303
+ fs.rmSync(path.join(missionsDir, `${missionId}.yml`), { force: true });
1304
+ });
1305
+
1306
+ const manifest = {
1307
+ schema_version: snapshot.schema_version || 1,
1308
+ generated_at: snapshot.generated_at || null,
1309
+ snapshot_hash: snapshot.snapshot_hash || null,
1310
+ project: snapshot.project ? scrubBootstrapValue(snapshot.project) : null,
1311
+ repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
1312
+ active_mission_ids: currentMissionIds,
1313
+ counts: snapshot.counts && typeof snapshot.counts === "object" ? scrubBootstrapValue(snapshot.counts) : {
1314
+ active_missions: missions.length,
1315
+ },
1316
+ };
1317
+ writeJsonFile(manifestPath, manifest);
1318
+ writeJsonFile(path.join(qaqcRoot, "latest-batch.json"), snapshot.latest_batch ? scrubBootstrapValue(snapshot.latest_batch) : null);
1319
+
1320
+ return {
1321
+ targetRoot,
1322
+ dataRoot,
1323
+ manifest,
1324
+ };
1325
+ }
1326
+
877
1327
  async function runCreatePrd(args) {
878
1328
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
879
1329
  if (!key) {
@@ -1126,6 +1576,97 @@ async function runBootstrap(args) {
1126
1576
  console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
1127
1577
  }
1128
1578
 
1579
+ async function runSyncQaqc(args) {
1580
+ const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
1581
+ if (!key) {
1582
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
1583
+ process.exit(1);
1584
+ }
1585
+
1586
+ const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
1587
+ const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
1588
+ const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
1589
+
1590
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
1591
+ const apiBase = normalizeApiBase(baseRaw);
1592
+
1593
+ let snapshot;
1594
+ try {
1595
+ snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
1596
+ } catch (err) {
1597
+ console.error("Failed to fetch QAQC sync snapshot:", err?.message || err);
1598
+ process.exit(1);
1599
+ }
1600
+
1601
+ if (args["print-context"] || args.printContext) {
1602
+ console.log(JSON.stringify(snapshot, null, 2));
1603
+ return;
1604
+ }
1605
+
1606
+ let resolved;
1607
+ try {
1608
+ resolved = resolveBootstrapWorkspace(snapshot.repo_names || []);
1609
+ } catch (err) {
1610
+ console.error(err?.message || err);
1611
+ process.exit(1);
1612
+ }
1613
+
1614
+ const wrapperRoot = resolved.root;
1615
+ const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
1616
+ const dryRun = Boolean(args["dry-run"] || args.dryRun);
1617
+ const summary = {
1618
+ api_base: apiBase,
1619
+ project_id: snapshot?.project?.id || null,
1620
+ wrapper_root: wrapperRoot,
1621
+ output_root: outputDir ? path.resolve(process.cwd(), String(outputDir)) : path.join(wrapperRoot, "MyteCommandCenter"),
1622
+ repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
1623
+ local: {
1624
+ mode: resolved.mode,
1625
+ found: (resolved.repos || []).map((repo) => repo.name),
1626
+ missing: resolved.missing || [],
1627
+ },
1628
+ counts: snapshot.counts && typeof snapshot.counts === "object" ? snapshot.counts : {
1629
+ active_missions: Array.isArray(snapshot.missions) ? snapshot.missions.length : 0,
1630
+ },
1631
+ snapshot_hash: snapshot.snapshot_hash || null,
1632
+ generated_at: snapshot.generated_at || null,
1633
+ dry_run: dryRun,
1634
+ };
1635
+
1636
+ if (dryRun) {
1637
+ if (args.json) {
1638
+ console.log(JSON.stringify(summary, null, 2));
1639
+ } else {
1640
+ console.log(`Project: ${summary.project_id || "(unknown)"}`);
1641
+ console.log(`Wrapper root: ${summary.wrapper_root}`);
1642
+ console.log(`Output root: ${summary.output_root}`);
1643
+ console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
1644
+ console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
1645
+ if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
1646
+ console.log(`Counts: active_missions=${summary.counts.active_missions || 0}, todo=${summary.counts.todo || 0}, in_progress=${summary.counts.in_progress || 0}, with_failures=${summary.counts.with_failures || 0}`);
1647
+ console.log("Dry run only - no files written.");
1648
+ }
1649
+ return;
1650
+ }
1651
+
1652
+ const writeResult = writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir });
1653
+ summary.data_root = writeResult.dataRoot;
1654
+
1655
+ if (args.json) {
1656
+ console.log(JSON.stringify(summary, null, 2));
1657
+ return;
1658
+ }
1659
+
1660
+ console.log(`Project: ${summary.project_id || "(unknown)"}`);
1661
+ console.log(`Wrapper root: ${summary.wrapper_root}`);
1662
+ console.log(`Output root: ${summary.output_root}`);
1663
+ console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
1664
+ console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
1665
+ if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
1666
+ console.log(`Wrote QAQC: active_missions=${summary.counts.active_missions || 0}, todo=${summary.counts.todo || 0}, in_progress=${summary.counts.in_progress || 0}, with_failures=${summary.counts.with_failures || 0}`);
1667
+ console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
1668
+ }
1669
+
1129
1670
  async function runQuery(args) {
1130
1671
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
1131
1672
  if (!key) {
@@ -1264,6 +1805,11 @@ async function main() {
1264
1805
  return;
1265
1806
  }
1266
1807
 
1808
+ if (command === "sync-qaqc" || command === "qaqc-sync") {
1809
+ await runSyncQaqc(args);
1810
+ return;
1811
+ }
1812
+
1267
1813
  if (command === "chat") {
1268
1814
  await runChat(args);
1269
1815
  return;
@@ -1274,6 +1820,16 @@ async function main() {
1274
1820
  return;
1275
1821
  }
1276
1822
 
1823
+ if (command === "update-team") {
1824
+ await runUpdateTeam(args);
1825
+ return;
1826
+ }
1827
+
1828
+ if (command === "update-client") {
1829
+ await runUpdateClient(args);
1830
+ return;
1831
+ }
1832
+
1277
1833
  // query/ask default
1278
1834
  await runQuery(args);
1279
1835
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Myte CLI core implementation (Project Assistant + deterministic diffs).",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",