@mytegroupinc/myte-core 0.0.5 → 0.0.6

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 +11 -0
  2. package/cli.js +234 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,11 +4,14 @@ 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`
9
10
  - `npm i -g myte` then `myte bootstrap`
11
+ - `npm i -g myte` then `myte sync-qaqc`
10
12
  - `npm i -g myte` then `myte query "..." --with-diff`
11
13
  - `npx myte@latest bootstrap`
14
+ - `npx myte@latest sync-qaqc`
12
15
  - `npx myte@latest query "..." --with-diff`
13
16
  - `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
14
17
  - `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
@@ -26,6 +29,12 @@ Notes:
26
29
  - `bootstrap` is a local file materialization path, not a hosted file download.
27
30
  - `bootstrap` expects to run from a wrapper root that contains the project's configured repo folders.
28
31
  - `bootstrap` writes `MyteCommandCenter/data/phases`, `epics`, `stories`, `missions`, `project.yml`, and `bootstrap-manifest.json`.
32
+ - `bootstrap` materializes a public Command Center DTO, not raw backend documents.
33
+ - `bootstrap` excludes internal keys like `_id`, `org_id`, `project_id`, `created_by`, `assigned_to`, and raw `qa_qc_results`.
34
+ - `sync-qaqc` works without `bootstrap`; it creates `MyteCommandCenter/data/qaqc` automatically if missing.
35
+ - `sync-qaqc` writes active mission QAQC cards to `MyteCommandCenter/data/qaqc/active-missions` and refreshes matching `MyteCommandCenter/data/missions` cards.
36
+ - `sync-qaqc` only exports active `Todo` / `In Progress` missions plus a public QAQC summary and sanitized latest batch metadata.
37
+ - `sync-qaqc` removes previously QAQC-managed mission files from `MyteCommandCenter/data/missions` once they leave the active set.
29
38
  - `create-prd` is a deterministic PRD upload path, not an LLM generation command.
30
39
  - `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
31
40
  - `--with-diff` includes per-repo diagnostics in `print-context` payload:
@@ -44,7 +53,9 @@ Deterministic `create-prd` contract:
44
53
 
45
54
  Examples:
46
55
  - `npx myte bootstrap`
56
+ - `npx myte sync-qaqc`
47
57
  - `npx myte bootstrap --dry-run --json`
58
+ - `npx myte sync-qaqc --dry-run --json`
48
59
  - `npx myte create-prd ./drafts/auth-prd.md --description "Short card summary"`
49
60
  - `npx myte create-prd ./drafts/auth-prd.md --print-context`
50
61
 
package/cli.js CHANGED
@@ -49,7 +49,7 @@ 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(["query", "ask", "chat", "config", "bootstrap", "sync-qaqc", "qaqc-sync", "create-prd", "add-prd", "prd", "help", "--help", "-h"]);
53
53
  const first = argv[0];
54
54
  if (first && known.has(first)) {
55
55
  const cmd = first === "--help" || first === "-h" ? "help" : first;
@@ -117,6 +117,7 @@ function printHelp() {
117
117
  " myte query \"<text>\" [--with-diff] [--context \"...\"]",
118
118
  " myte config [--json]",
119
119
  " myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
120
+ " myte sync-qaqc [--output-dir ./MyteCommandCenter] [--json]",
120
121
  " myte chat",
121
122
  " myte create-prd <file.md> [--json] [--title \"...\"] [--description \"...\"]",
122
123
  " myte add-prd <file.md> [--json]",
@@ -136,6 +137,12 @@ function printHelp() {
136
137
  " - Writes MyteCommandCenter/data/phases, epics, stories, and missions locally",
137
138
  " - Uses the project-scoped bootstrap snapshot from the Myte API",
138
139
  "",
140
+ "sync-qaqc contract:",
141
+ " - Run from the wrapper root that contains the project's configured repo folders",
142
+ " - Works even if bootstrap has not been run yet; it creates MyteCommandCenter/data/qaqc automatically",
143
+ " - Writes active mission QAQC context under MyteCommandCenter/data/qaqc/active-missions",
144
+ " - Refreshes matching MyteCommandCenter/data/missions cards for active missions only",
145
+ "",
139
146
  "create-prd contract:",
140
147
  " - Required: valid MYTE_API_KEY, PRD markdown body, title",
141
148
  " - Title source: myte-kanban.title, first # heading, or --title",
@@ -158,6 +165,7 @@ function printHelp() {
158
165
  " myte query \"What changed in logging?\" --with-diff",
159
166
  " myte bootstrap",
160
167
  " myte bootstrap --output-dir ./MyteCommandCenter",
168
+ " myte sync-qaqc",
161
169
  " myte create-prd ./drafts/auth-prd.md --description \"Short card summary\"",
162
170
  " cat ./drafts/auth-prd.md | myte create-prd --stdin",
163
171
  " myte config",
@@ -733,6 +741,28 @@ async function fetchBootstrapSnapshot({ apiBase, key, timeoutMs }) {
733
741
  return body.data || {};
734
742
  }
735
743
 
744
+ async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
745
+ const fetchFn = await getFetch();
746
+ const url = `${apiBase}/project-assistant/qaqc-sync`;
747
+ const { resp, body } = await fetchJsonWithTimeout(
748
+ fetchFn,
749
+ url,
750
+ {
751
+ method: "GET",
752
+ headers: { Authorization: `Bearer ${key}` },
753
+ },
754
+ timeoutMs
755
+ );
756
+
757
+ if (!resp.ok || body.status !== "success") {
758
+ const msg = body?.message || `QAQC sync request failed (${resp.status})`;
759
+ const err = new Error(msg);
760
+ err.status = resp.status;
761
+ throw err;
762
+ }
763
+ return body.data || {};
764
+ }
765
+
736
766
  async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
737
767
  const fetchFn = await getFetch();
738
768
  const url = `${apiBase}${endpoint}`;
@@ -799,6 +829,46 @@ function writeJsonFile(filePath, value) {
799
829
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
800
830
  }
801
831
 
832
+ function readJsonFile(filePath) {
833
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
834
+ try {
835
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
836
+ } catch {
837
+ return null;
838
+ }
839
+ }
840
+
841
+ const BOOTSTRAP_FORBIDDEN_KEYS = new Set([
842
+ "_id",
843
+ "org_id",
844
+ "project_id",
845
+ "created_by",
846
+ "assigned_to",
847
+ "user_id",
848
+ "qa_qc_results",
849
+ "job_id",
850
+ "job_ids",
851
+ "celery_task_id",
852
+ "conversation_id",
853
+ "error",
854
+ ]);
855
+
856
+ function scrubBootstrapValue(value) {
857
+ if (Array.isArray(value)) {
858
+ return value.map((item) => scrubBootstrapValue(item));
859
+ }
860
+ if (!value || typeof value !== "object") {
861
+ return value;
862
+ }
863
+
864
+ const cleaned = {};
865
+ for (const [key, child] of Object.entries(value)) {
866
+ if (BOOTSTRAP_FORBIDDEN_KEYS.has(key)) continue;
867
+ cleaned[key] = scrubBootstrapValue(child);
868
+ }
869
+ return cleaned;
870
+ }
871
+
802
872
  function resolveBootstrapWorkspace(repoNames) {
803
873
  const resolved = resolveConfiguredRepos(repoNames);
804
874
  if (!resolved.root || !Array.isArray(resolved.repos) || !resolved.repos.length) {
@@ -826,10 +896,10 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
826
896
  clearYamlDirectory(storiesDir);
827
897
  clearYamlDirectory(missionsDir);
828
898
 
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 : [];
899
+ const phases = Array.isArray(snapshot.phases) ? snapshot.phases.map((item) => scrubBootstrapValue(item)) : [];
900
+ const epics = Array.isArray(snapshot.epics) ? snapshot.epics.map((item) => scrubBootstrapValue(item)) : [];
901
+ const stories = Array.isArray(snapshot.stories) ? snapshot.stories.map((item) => scrubBootstrapValue(item)) : [];
902
+ const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
833
903
 
834
904
  phases.forEach((phase, index) => {
835
905
  const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
@@ -844,19 +914,19 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
844
914
  writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
845
915
  });
846
916
  missions.forEach((mission, index) => {
847
- const missionId = stableItemId(mission, ["mission_id", "id", "_id"], `M${String(index + 1).padStart(3, "0")}`);
917
+ const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
848
918
  writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
849
919
  });
850
920
 
851
921
  if (snapshot.project && typeof snapshot.project === "object") {
852
- writeYamlFile(path.join(dataRoot, "project.yml"), snapshot.project);
922
+ writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
853
923
  }
854
924
 
855
925
  const manifest = {
856
926
  schema_version: snapshot.schema_version || 1,
857
927
  generated_at: snapshot.generated_at || null,
858
928
  snapshot_hash: snapshot.snapshot_hash || null,
859
- project: snapshot.project || null,
929
+ project: snapshot.project ? scrubBootstrapValue(snapshot.project) : null,
860
930
  repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
861
931
  counts: {
862
932
  phases: phases.length,
@@ -874,6 +944,66 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
874
944
  };
875
945
  }
876
946
 
947
+ function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
948
+ const targetRoot = outputDir
949
+ ? path.resolve(process.cwd(), String(outputDir))
950
+ : path.join(wrapperRoot, "MyteCommandCenter");
951
+ const dataRoot = path.join(targetRoot, "data");
952
+ const missionsDir = path.join(dataRoot, "missions");
953
+ const qaqcRoot = path.join(dataRoot, "qaqc");
954
+ const activeMissionsDir = path.join(qaqcRoot, "active-missions");
955
+ const manifestPath = path.join(qaqcRoot, "manifest.json");
956
+
957
+ ensureDir(dataRoot);
958
+ ensureDir(missionsDir);
959
+ ensureDir(qaqcRoot);
960
+ const previousManifest = readJsonFile(manifestPath);
961
+ const previousMissionIds = Array.isArray(previousManifest?.active_mission_ids)
962
+ ? previousManifest.active_mission_ids.map((item) => String(item).trim()).filter(Boolean)
963
+ : [];
964
+ clearYamlDirectory(activeMissionsDir);
965
+
966
+ const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
967
+ const currentMissionIds = [];
968
+
969
+ if (snapshot.project && typeof snapshot.project === "object") {
970
+ writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
971
+ }
972
+
973
+ missions.forEach((mission, index) => {
974
+ const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
975
+ currentMissionIds.push(missionId);
976
+ writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
977
+ writeYamlFile(path.join(activeMissionsDir, `${missionId}.yml`), mission);
978
+ });
979
+
980
+ const currentMissionIdSet = new Set(currentMissionIds);
981
+ previousMissionIds.forEach((missionId) => {
982
+ if (!missionId || currentMissionIdSet.has(missionId)) return;
983
+ fs.rmSync(path.join(missionsDir, `${missionId}.yml`), { force: true });
984
+ });
985
+
986
+ const manifest = {
987
+ schema_version: snapshot.schema_version || 1,
988
+ generated_at: snapshot.generated_at || null,
989
+ snapshot_hash: snapshot.snapshot_hash || null,
990
+ project: snapshot.project ? scrubBootstrapValue(snapshot.project) : null,
991
+ repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
992
+ active_mission_ids: currentMissionIds,
993
+ counts: snapshot.counts && typeof snapshot.counts === "object" ? scrubBootstrapValue(snapshot.counts) : {
994
+ active_missions: missions.length,
995
+ },
996
+ };
997
+ writeJsonFile(manifestPath, manifest);
998
+ writeJsonFile(path.join(qaqcRoot, "latest-batch.json"), snapshot.latest_batch ? scrubBootstrapValue(snapshot.latest_batch) : null);
999
+
1000
+ return {
1001
+ targetRoot,
1002
+ dataRoot,
1003
+ manifest,
1004
+ };
1005
+ }
1006
+
877
1007
  async function runCreatePrd(args) {
878
1008
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
879
1009
  if (!key) {
@@ -1126,6 +1256,97 @@ async function runBootstrap(args) {
1126
1256
  console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
1127
1257
  }
1128
1258
 
1259
+ async function runSyncQaqc(args) {
1260
+ const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
1261
+ if (!key) {
1262
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
1263
+ process.exit(1);
1264
+ }
1265
+
1266
+ const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
1267
+ const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
1268
+ const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
1269
+
1270
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
1271
+ const apiBase = normalizeApiBase(baseRaw);
1272
+
1273
+ let snapshot;
1274
+ try {
1275
+ snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
1276
+ } catch (err) {
1277
+ console.error("Failed to fetch QAQC sync snapshot:", err?.message || err);
1278
+ process.exit(1);
1279
+ }
1280
+
1281
+ if (args["print-context"] || args.printContext) {
1282
+ console.log(JSON.stringify(snapshot, null, 2));
1283
+ return;
1284
+ }
1285
+
1286
+ let resolved;
1287
+ try {
1288
+ resolved = resolveBootstrapWorkspace(snapshot.repo_names || []);
1289
+ } catch (err) {
1290
+ console.error(err?.message || err);
1291
+ process.exit(1);
1292
+ }
1293
+
1294
+ const wrapperRoot = resolved.root;
1295
+ const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
1296
+ const dryRun = Boolean(args["dry-run"] || args.dryRun);
1297
+ const summary = {
1298
+ api_base: apiBase,
1299
+ project_id: snapshot?.project?.id || null,
1300
+ wrapper_root: wrapperRoot,
1301
+ output_root: outputDir ? path.resolve(process.cwd(), String(outputDir)) : path.join(wrapperRoot, "MyteCommandCenter"),
1302
+ repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
1303
+ local: {
1304
+ mode: resolved.mode,
1305
+ found: (resolved.repos || []).map((repo) => repo.name),
1306
+ missing: resolved.missing || [],
1307
+ },
1308
+ counts: snapshot.counts && typeof snapshot.counts === "object" ? snapshot.counts : {
1309
+ active_missions: Array.isArray(snapshot.missions) ? snapshot.missions.length : 0,
1310
+ },
1311
+ snapshot_hash: snapshot.snapshot_hash || null,
1312
+ generated_at: snapshot.generated_at || null,
1313
+ dry_run: dryRun,
1314
+ };
1315
+
1316
+ if (dryRun) {
1317
+ if (args.json) {
1318
+ console.log(JSON.stringify(summary, null, 2));
1319
+ } else {
1320
+ console.log(`Project: ${summary.project_id || "(unknown)"}`);
1321
+ console.log(`Wrapper root: ${summary.wrapper_root}`);
1322
+ console.log(`Output root: ${summary.output_root}`);
1323
+ console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
1324
+ console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
1325
+ if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
1326
+ 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}`);
1327
+ console.log("Dry run only - no files written.");
1328
+ }
1329
+ return;
1330
+ }
1331
+
1332
+ const writeResult = writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir });
1333
+ summary.data_root = writeResult.dataRoot;
1334
+
1335
+ if (args.json) {
1336
+ console.log(JSON.stringify(summary, null, 2));
1337
+ return;
1338
+ }
1339
+
1340
+ console.log(`Project: ${summary.project_id || "(unknown)"}`);
1341
+ console.log(`Wrapper root: ${summary.wrapper_root}`);
1342
+ console.log(`Output root: ${summary.output_root}`);
1343
+ console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
1344
+ console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
1345
+ if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
1346
+ 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}`);
1347
+ console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
1348
+ }
1349
+
1129
1350
  async function runQuery(args) {
1130
1351
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
1131
1352
  if (!key) {
@@ -1264,6 +1485,11 @@ async function main() {
1264
1485
  return;
1265
1486
  }
1266
1487
 
1488
+ if (command === "sync-qaqc" || command === "qaqc-sync") {
1489
+ await runSyncQaqc(args);
1490
+ return;
1491
+ }
1492
+
1267
1493
  if (command === "chat") {
1268
1494
  await runChat(args);
1269
1495
  return;
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.6",
4
4
  "description": "Myte CLI core implementation (Project Assistant + deterministic diffs).",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",