@mytegroupinc/myte-core 0.0.20 → 0.0.21

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 +1 -0
  2. package/cli.js +524 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,5 +19,6 @@ This package exists so the public wrapper can stay small and versioned cleanly.
19
19
  ## Behavior Summary
20
20
 
21
21
  - Snapshot-style commands such as `bootstrap`, `sync-qaqc`, `feedback-sync`, and `suggestions sync` write local `MyteCommandCenter` data.
22
+ - `feedback status|edit|assign|archive` writes reviewable local YAML artifacts, while `feedback validate|apply` sends those artifacts to the backend so business rules stay server-side.
22
23
  - `query --with-diff` requires project repos to be configured for diff collection and fails fast when no matching local project repo can be resolved.
23
24
  - Public package documentation is intentionally minimal. Internal rollout and design notes are not part of the npm package contract.
package/cli.js CHANGED
@@ -98,6 +98,7 @@ function splitCommand(argv) {
98
98
  "update-owner",
99
99
  "update-client",
100
100
  "feedback-sync",
101
+ "feedback",
101
102
  "help",
102
103
  "--help",
103
104
  "-h",
@@ -141,6 +142,17 @@ function parseArgs(argv) {
141
142
  "target-contact-ids",
142
143
  "status",
143
144
  "source",
145
+ "feedback-id",
146
+ "reason",
147
+ "review-action",
148
+ "idempotency-key",
149
+ "user-id",
150
+ "assigned-user-id",
151
+ "due-date",
152
+ "priority",
153
+ "review-note",
154
+ "tags",
155
+ "tag",
144
156
  "mission-ids",
145
157
  "client-session-id",
146
158
  ],
@@ -212,6 +224,10 @@ function printHelp() {
212
224
  " myte update-owner --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--json]",
213
225
  " myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] [--json]",
214
226
  " myte feedback-sync [--status <value>] [--source <value>] [--with-prd-text|--no-with-prd-text] [--output-dir ./MyteCommandCenter] [--json]",
227
+ " myte feedback status --feedback-id <id> --status todo|in_progress|in_review|completed|deployed|rejected|archived --reason \"...\"",
228
+ " myte feedback edit --feedback-id <id> [--title \"...\"] [--feedback-text \"...\"] [--priority High] [--reason \"...\"]",
229
+ " myte feedback validate --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
230
+ " myte feedback apply --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
215
231
  " myte create-prd <file.md> [more.md ...] [--json] [--title \"...\"] [--description \"...\"]",
216
232
  " cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
217
233
  " cat update.md | myte update-owner --stdin --subject \"Owner update\"",
@@ -290,6 +306,12 @@ function printHelp() {
290
306
  " - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
291
307
  " - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
292
308
  "",
309
+ "feedback review contract:",
310
+ " - Draft commands write review artifacts under MyteCommandCenter/reviews/feedback/*.yml for local IDE diff review",
311
+ " - validate/apply send those artifacts to /api/project-assistant/feedback/<id>/refinement/*",
312
+ " - The backend owns authorization, stale snapshot checks, allowed field/transition rules, and history",
313
+ " - apply is idempotent and does not rewrite local feedback.yml; run feedback-sync after apply to refresh local state",
314
+ "",
293
315
  "Options:",
294
316
  " --with-diff Include deterministic git diffs (project-scoped; fails fast if no project repos are configured or resolved)",
295
317
  " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
@@ -310,8 +332,15 @@ function printHelp() {
310
332
  " --body-file <path> Read update-owner or update-client markdown body from a file",
311
333
  " --target-contact-id Add one client contact ObjectId (repeatable)",
312
334
  " --target-contact-ids Comma-separated client contact ObjectIds",
313
- " --status <value> For mission status: required target status (todo|in_progress|done). For feedback-sync: optional filter (default: Pending).",
335
+ " --feedback-id <id> Feedback ObjectId for feedback review commands",
336
+ " --reason <text> Human review reason included in feedback refinement artifacts",
337
+ " --status <value> Mission status target, feedback-sync filter, or feedback review state depending on command",
314
338
  " --source <value> Feedback source filter for feedback-sync",
339
+ " --priority <value> Feedback review priority target: Low, Medium, or High",
340
+ " --user-id <id> Feedback assignee user id for `myte feedback assign`",
341
+ " --assigned-user-id Alias for --user-id in feedback assign",
342
+ " --due-date <date> Feedback due date target for edit artifacts",
343
+ " --review-note <text> Review note stored in feedback refinement history",
315
344
  " --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
316
345
  " --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
317
346
  " --mission-ids <ids> Comma-separated mission business ids for run-qaqc or mission status (quote multi-id values on PowerShell)",
@@ -340,6 +369,9 @@ function printHelp() {
340
369
  " myte update-owner --subject \"QAQC progress\" --body-file ./updates/owner.md",
341
370
  " myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md",
342
371
  " myte feedback-sync --json",
372
+ " myte feedback status --feedback-id 507f1f77bcf86cd799439011 --status in_review --reason \"Ready for owner review\"",
373
+ " myte feedback validate --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
374
+ " myte feedback apply --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
343
375
  " myte suggestions create --file ./suggestions/create.yml",
344
376
  " myte suggestions revise",
345
377
  " myte suggestions review",
@@ -1079,21 +1111,35 @@ function resolveConfiguredRepoBindings(bindings) {
1079
1111
 
1080
1112
  const cwd = process.cwd();
1081
1113
  const currentRepoRoot = findGitTopLevel(cwd);
1082
- const scanStart = currentRepoRoot ? path.dirname(currentRepoRoot) : cwd;
1114
+ const searchRoots = [];
1115
+ const seenRoots = new Set();
1116
+ const pushSearchRoot = (value) => {
1117
+ if (!value) return;
1118
+ const resolved = path.resolve(value);
1119
+ if (seenRoots.has(resolved)) return;
1120
+ seenRoots.add(resolved);
1121
+ searchRoots.push(resolved);
1122
+ };
1083
1123
 
1084
- const ancestors = [];
1124
+ // Prefer the invocation workspace first. A wrapper repo can contain the
1125
+ // configured API/WEB repos as children, so jumping immediately to the parent
1126
+ // can miss the project repos entirely.
1127
+ pushSearchRoot(cwd);
1128
+ if (currentRepoRoot) pushSearchRoot(currentRepoRoot);
1129
+
1130
+ const scanStart = currentRepoRoot ? path.dirname(currentRepoRoot) : path.dirname(cwd);
1085
1131
  let cur = scanStart;
1086
1132
  for (let i = 0; i < 8; i += 1) {
1087
- ancestors.push(cur);
1133
+ pushSearchRoot(cur);
1088
1134
  const parent = path.dirname(cur);
1089
1135
  if (parent === cur) break;
1090
1136
  cur = parent;
1091
1137
  }
1092
1138
 
1093
- let fallbackCandidates = currentRepoRoot ? listGitRepoCandidates(path.dirname(currentRepoRoot), currentRepoRoot) : [];
1094
- let fallbackRoot = currentRepoRoot ? path.dirname(currentRepoRoot) : null;
1139
+ let fallbackCandidates = [];
1140
+ let fallbackRoot = null;
1095
1141
 
1096
- for (const candidateRoot of ancestors) {
1142
+ for (const candidateRoot of searchRoots) {
1097
1143
  const candidates = listGitRepoCandidates(candidateRoot, currentRepoRoot);
1098
1144
  if (!fallbackCandidates.length && candidates.length) {
1099
1145
  fallbackCandidates = candidates;
@@ -1585,6 +1631,83 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
1585
1631
  return body.data || {};
1586
1632
  }
1587
1633
 
1634
+ async function fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId }) {
1635
+ const fetchFn = await getFetch();
1636
+ const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}`;
1637
+ const { resp, body } = await fetchJsonWithTimeout(
1638
+ fetchFn,
1639
+ url,
1640
+ {
1641
+ method: "GET",
1642
+ headers: { Authorization: `Bearer ${key}` },
1643
+ },
1644
+ timeoutMs
1645
+ );
1646
+
1647
+ if (!resp.ok || body.status !== "success") {
1648
+ const msg = body?.message || `Feedback review request failed (${resp.status})`;
1649
+ const err = new Error(msg);
1650
+ err.status = resp.status;
1651
+ throw err;
1652
+ }
1653
+ return body.data || {};
1654
+ }
1655
+
1656
+ async function fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId }) {
1657
+ const fetchFn = await getFetch();
1658
+ const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/history`;
1659
+ const { resp, body } = await fetchJsonWithTimeout(
1660
+ fetchFn,
1661
+ url,
1662
+ {
1663
+ method: "GET",
1664
+ headers: { Authorization: `Bearer ${key}` },
1665
+ },
1666
+ timeoutMs
1667
+ );
1668
+
1669
+ if (!resp.ok || body.status !== "success") {
1670
+ const msg = body?.message || `Feedback history request failed (${resp.status})`;
1671
+ const err = new Error(msg);
1672
+ err.status = resp.status;
1673
+ throw err;
1674
+ }
1675
+ return body.data || {};
1676
+ }
1677
+
1678
+ async function postFeedbackRefinement({ apiBase, key, timeoutMs, feedbackId, mode, payload, idempotencyKey, clientSessionId }) {
1679
+ const fetchFn = await getFetch();
1680
+ const action = String(mode || "").trim();
1681
+ const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/${action}`;
1682
+ const headers = {
1683
+ "Content-Type": "application/json",
1684
+ Authorization: `Bearer ${key}`,
1685
+ ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
1686
+ };
1687
+ if (idempotencyKey) {
1688
+ headers["X-Idempotency-Key"] = String(idempotencyKey).trim();
1689
+ }
1690
+ const { resp, body } = await fetchJsonWithTimeout(
1691
+ fetchFn,
1692
+ url,
1693
+ {
1694
+ method: "POST",
1695
+ headers,
1696
+ body: JSON.stringify(payload || {}),
1697
+ },
1698
+ timeoutMs
1699
+ );
1700
+
1701
+ if (!resp.ok || body.status !== "success") {
1702
+ const msg = body?.message || `Feedback refinement ${action} failed (${resp.status})`;
1703
+ const err = new Error(msg);
1704
+ err.status = resp.status;
1705
+ err.data = body?.data;
1706
+ throw err;
1707
+ }
1708
+ return body.data || {};
1709
+ }
1710
+
1588
1711
  async function fetchSuggestionsSyncSnapshot({ apiBase, key, timeoutMs, actorScope = "" }) {
1589
1712
  const fetchFn = await getFetch();
1590
1713
  const url = new URL(`${apiBase}/project-assistant/suggestions`);
@@ -2756,6 +2879,150 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
2756
2879
  };
2757
2880
  }
2758
2881
 
2882
+ function resolveFeedbackReviewPaths(args, { requireFeedback = false } = {}) {
2883
+ const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
2884
+ const targetRoot = outputDir
2885
+ ? path.resolve(process.cwd(), String(outputDir))
2886
+ : findExistingCommandCenterRoot(process.cwd());
2887
+ if (!targetRoot) {
2888
+ if (requireFeedback) {
2889
+ console.error("feedback.yml was not found. Run `myte feedback-sync` first or pass --output-dir.");
2890
+ process.exit(1);
2891
+ }
2892
+ return null;
2893
+ }
2894
+ const dataRoot = path.join(targetRoot, "data");
2895
+ const feedbackPath = path.join(dataRoot, "feedback.yml");
2896
+ if (requireFeedback && !fs.existsSync(feedbackPath)) {
2897
+ console.error("feedback.yml was not found. Run `myte feedback-sync` first or pass --output-dir.");
2898
+ process.exit(1);
2899
+ }
2900
+ return {
2901
+ targetRoot,
2902
+ dataRoot,
2903
+ feedbackPath,
2904
+ reviewsRoot: path.join(targetRoot, "reviews", "feedback"),
2905
+ wrapperRoot: path.dirname(targetRoot),
2906
+ };
2907
+ }
2908
+
2909
+ function readFeedbackManifest(args) {
2910
+ const paths = resolveFeedbackReviewPaths(args, { requireFeedback: true });
2911
+ const manifest = readYamlFile(paths.feedbackPath) || {};
2912
+ if (!isPlainObject(manifest)) {
2913
+ console.error(`Invalid feedback manifest: ${paths.feedbackPath}`);
2914
+ process.exit(1);
2915
+ }
2916
+ return { paths, manifest };
2917
+ }
2918
+
2919
+ function findFeedbackManifestItem(manifest, feedbackId) {
2920
+ const wanted = String(feedbackId || "").trim();
2921
+ if (!wanted) return null;
2922
+ const items = Array.isArray(manifest?.items) ? manifest.items : [];
2923
+ return items.find((item) => String(item?.feedback_id || item?.id || "").trim() === wanted) || null;
2924
+ }
2925
+
2926
+ function readFeedbackRefinementArtifact(args) {
2927
+ const filePath = firstNonEmptyString(args.file);
2928
+ if (!filePath) {
2929
+ console.error("Missing --file for feedback validate/apply.");
2930
+ process.exit(1);
2931
+ }
2932
+ const absPath = resolveInputFile(filePath, "Feedback refinement");
2933
+ const text = fs.readFileSync(absPath, "utf8");
2934
+ let payload;
2935
+ if (absPath.toLowerCase().endsWith(".json")) {
2936
+ payload = JSON.parse(text);
2937
+ } else {
2938
+ payload = parseYaml(text);
2939
+ }
2940
+ if (!isPlainObject(payload)) {
2941
+ console.error(`Feedback refinement file must contain an object: ${absPath}`);
2942
+ process.exit(1);
2943
+ }
2944
+ return { absPath, payload };
2945
+ }
2946
+
2947
+ function parseFeedbackTags(args) {
2948
+ const values = parseCsvValues(args.tags, args.tag);
2949
+ return values.length ? values : undefined;
2950
+ }
2951
+
2952
+ function feedbackChange(from, to) {
2953
+ return { from: from === undefined ? null : from, to: to === undefined ? null : to };
2954
+ }
2955
+
2956
+ function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
2957
+ const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
2958
+ const artifact = {
2959
+ schema_version: 1,
2960
+ kind: "feedback_refinement",
2961
+ project_id: firstNonEmptyString(manifest?.project?.id, item?.project_id),
2962
+ feedback_id: feedbackId,
2963
+ title_snapshot: firstNonEmptyString(item?.title),
2964
+ base_snapshot_hash: firstNonEmptyString(item?.snapshot_hash),
2965
+ base_updated_at: firstNonEmptyString(item?.updated_at),
2966
+ review_action: action,
2967
+ reason: reason || null,
2968
+ changes,
2969
+ force: Boolean(args.force) || undefined,
2970
+ client_session_id: clientSessionId || undefined,
2971
+ created_at: new Date().toISOString(),
2972
+ };
2973
+ return Object.fromEntries(Object.entries(artifact).filter(([, value]) => value !== undefined));
2974
+ }
2975
+
2976
+ function writeFeedbackRefinementArtifact({ paths, artifact, action }) {
2977
+ const feedbackId = sanitizeFileSegment(artifact.feedback_id, "feedback");
2978
+ const actionSegment = sanitizeFileSegment(action || artifact.review_action || "refine", "refine");
2979
+ const filePath = path.join(paths.reviewsRoot, `${feedbackId}-${actionSegment}.yml`);
2980
+ writeYamlFile(filePath, artifact);
2981
+ return filePath;
2982
+ }
2983
+
2984
+ function printFeedbackRefinementDraft({ artifact, filePath, args }) {
2985
+ const output = {
2986
+ feedback_id: artifact.feedback_id,
2987
+ review_action: artifact.review_action,
2988
+ artifact_path: filePath || null,
2989
+ base_snapshot_hash: artifact.base_snapshot_hash || null,
2990
+ changes: artifact.changes || {},
2991
+ reason: artifact.reason || null,
2992
+ };
2993
+ if (args.json) {
2994
+ console.log(JSON.stringify(output, null, 2));
2995
+ return;
2996
+ }
2997
+ console.log(`Feedback: ${output.feedback_id}`);
2998
+ console.log(`Action: ${output.review_action}`);
2999
+ if (filePath) console.log(`Artifact: ${filePath}`);
3000
+ console.log(`Base Snapshot: ${output.base_snapshot_hash || "n/a"}`);
3001
+ console.log("Changes:");
3002
+ for (const [field, change] of Object.entries(output.changes || {})) {
3003
+ const fromValue = change && typeof change === "object" && "from" in change ? change.from : null;
3004
+ const toValue = change && typeof change === "object" && "to" in change ? change.to : change;
3005
+ console.log(`- ${field}: ${JSON.stringify(fromValue)} -> ${JSON.stringify(toValue)}`);
3006
+ }
3007
+ if (output.reason) console.log(`Reason: ${output.reason}`);
3008
+ }
3009
+
3010
+ function summarizeFeedbackRefinementResult(data) {
3011
+ return {
3012
+ valid: Boolean(data?.valid),
3013
+ feedback_id: data?.feedback_id || data?.feedback?.feedback_id || null,
3014
+ project_id: data?.project_id || null,
3015
+ previous_snapshot_hash: data?.previous_snapshot_hash || data?.base_snapshot_hash || null,
3016
+ current_snapshot_hash: data?.current_snapshot_hash || null,
3017
+ new_snapshot_hash: data?.new_snapshot_hash || data?.feedback?.snapshot_hash || null,
3018
+ diff_count: Array.isArray(data?.diffs) ? data.diffs.length : 0,
3019
+ diffs: data?.diffs || [],
3020
+ warnings: data?.warnings || [],
3021
+ errors: data?.errors || [],
3022
+ history_id: data?.history?.history_id || null,
3023
+ };
3024
+ }
3025
+
2759
3026
  function missionOpsThreads(payload) {
2760
3027
  if (Array.isArray(payload?.threads)) return payload.threads;
2761
3028
  if (Array.isArray(payload?.suggestions)) return payload.suggestions;
@@ -3929,6 +4196,251 @@ async function runFeedbackSync(args) {
3929
4196
  console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
3930
4197
  }
3931
4198
 
4199
+ function resolveFeedbackIdArg(args) {
4200
+ return firstNonEmptyString(args["feedback-id"], args.feedbackId, args.feedback_id, args._?.[1]);
4201
+ }
4202
+
4203
+ function requireFeedbackDraftContext(args) {
4204
+ const feedbackId = resolveFeedbackIdArg(args);
4205
+ if (!feedbackId) {
4206
+ console.error("Missing --feedback-id.");
4207
+ process.exit(1);
4208
+ }
4209
+ const { paths, manifest } = readFeedbackManifest(args);
4210
+ const item = findFeedbackManifestItem(manifest, feedbackId);
4211
+ if (!item) {
4212
+ console.error(`Feedback item not found in feedback.yml: ${feedbackId}. Run \`myte feedback-sync\` first.`);
4213
+ process.exit(1);
4214
+ }
4215
+ return { paths, manifest, item, feedbackId };
4216
+ }
4217
+
4218
+ async function buildFeedbackDraftForCommand(args, subcommand) {
4219
+ const { paths, manifest, item, feedbackId } = requireFeedbackDraftContext(args);
4220
+ const action = subcommand || "refine";
4221
+ const reason = firstNonEmptyString(args.reason);
4222
+ const changes = {};
4223
+
4224
+ if (action === "status") {
4225
+ const targetStatus = firstNonEmptyString(args.status, args._?.[2]);
4226
+ if (!targetStatus) {
4227
+ console.error("Missing --status for feedback status.");
4228
+ process.exit(1);
4229
+ }
4230
+ changes.feedback_state = feedbackChange(firstNonEmptyString(item.feedback_state, item.status), targetStatus);
4231
+ } else if (action === "archive") {
4232
+ changes.feedback_state = feedbackChange(firstNonEmptyString(item.feedback_state, item.status), "archived");
4233
+ } else if (action === "assign") {
4234
+ const assignee = firstNonEmptyString(args["assigned-user-id"], args.assignedUserId, args.assigned_user_id, args["user-id"], args.userId, args.user_id, args._?.[2]);
4235
+ if (!assignee) {
4236
+ console.error("Missing --user-id or --assigned-user-id for feedback assign.");
4237
+ process.exit(1);
4238
+ }
4239
+ changes.assigned_user_id = feedbackChange(firstNonEmptyString(item.assigned_user_id), assignee);
4240
+ } else if (action === "edit" || action === "refine") {
4241
+ const title = firstNonEmptyString(args.title);
4242
+ if (title) changes.title = feedbackChange(firstNonEmptyString(item.title), title);
4243
+
4244
+ let feedbackText = firstNonEmptyString(args["feedback-text"], args.feedbackText, args.feedback_text);
4245
+ const bodyFile = firstNonEmptyString(args["body-file"], args.bodyFile, args.body_file);
4246
+ if (!feedbackText && bodyFile) {
4247
+ const bodyPath = resolveInputFile(bodyFile, "Feedback body");
4248
+ feedbackText = fs.readFileSync(bodyPath, "utf8").trim();
4249
+ }
4250
+ if (feedbackText) changes.feedback_text = feedbackChange(firstNonEmptyString(item.feedback_text), feedbackText);
4251
+
4252
+ const priority = firstNonEmptyString(args.priority);
4253
+ if (priority) changes.priority = feedbackChange(firstNonEmptyString(item.priority), priority);
4254
+
4255
+ const dueDate = firstNonEmptyString(args["due-date"], args.dueDate, args.due_date);
4256
+ if (dueDate) changes.due_date = feedbackChange(firstNonEmptyString(item.due_date, item.due_date_text), dueDate);
4257
+
4258
+ const tags = parseFeedbackTags(args);
4259
+ if (tags) changes.tags = feedbackChange(Array.isArray(item.tags) ? item.tags : [], tags);
4260
+
4261
+ const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
4262
+ if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
4263
+ } else {
4264
+ console.error("Unknown feedback command. Use status, edit, assign, archive, get, history, validate, or apply.");
4265
+ process.exit(1);
4266
+ }
4267
+
4268
+ if (!Object.keys(changes).length) {
4269
+ console.error("No feedback changes were provided.");
4270
+ process.exit(1);
4271
+ }
4272
+
4273
+ const artifact = buildFeedbackRefinementArtifact({
4274
+ manifest,
4275
+ item,
4276
+ feedbackId,
4277
+ action,
4278
+ changes,
4279
+ reason,
4280
+ args,
4281
+ });
4282
+
4283
+ if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
4284
+ if (args.json) {
4285
+ console.log(JSON.stringify(artifact, null, 2));
4286
+ } else {
4287
+ console.log(stringifyYaml(artifact));
4288
+ }
4289
+ return;
4290
+ }
4291
+
4292
+ const filePath = writeFeedbackRefinementArtifact({ paths, artifact, action });
4293
+ printFeedbackRefinementDraft({ artifact, filePath, args });
4294
+ }
4295
+
4296
+ async function runFeedbackValidateOrApply(args, mode) {
4297
+ const key = getProjectApiKey();
4298
+ if (!key) {
4299
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
4300
+ process.exit(1);
4301
+ }
4302
+ const { absPath, payload } = readFeedbackRefinementArtifact(args);
4303
+ const feedbackId = firstNonEmptyString(args["feedback-id"], args.feedbackId, args.feedback_id, payload.feedback_id);
4304
+ if (!feedbackId) {
4305
+ console.error("Feedback refinement artifact is missing feedback_id.");
4306
+ process.exit(1);
4307
+ }
4308
+ if (args.force) payload.force = true;
4309
+
4310
+ const timeoutMs = resolveTimeoutMs(args);
4311
+ const apiBase = resolveApiBase(args);
4312
+ const clientSessionId = firstNonEmptyString(
4313
+ args["client-session-id"],
4314
+ args.clientSessionId,
4315
+ args.client_session_id,
4316
+ payload.client_session_id
4317
+ );
4318
+ const idempotencyKey = mode === "apply"
4319
+ ? resolveProjectMutationIdempotencyKey({
4320
+ args,
4321
+ operation: `feedback_refinement_apply:${feedbackId}`,
4322
+ payload,
4323
+ })
4324
+ : null;
4325
+
4326
+ let data;
4327
+ try {
4328
+ data = await postFeedbackRefinement({
4329
+ apiBase,
4330
+ key,
4331
+ timeoutMs,
4332
+ feedbackId,
4333
+ mode,
4334
+ payload,
4335
+ idempotencyKey,
4336
+ clientSessionId,
4337
+ });
4338
+ } catch (err) {
4339
+ if (args.json) {
4340
+ console.log(JSON.stringify({
4341
+ ok: false,
4342
+ status: err?.status || null,
4343
+ message: err?.message || String(err),
4344
+ data: err?.data || null,
4345
+ artifact_path: absPath,
4346
+ }, null, 2));
4347
+ } else {
4348
+ console.error(`Feedback ${mode} failed:`, err?.message || err);
4349
+ if (err?.data) console.error(JSON.stringify(err.data, null, 2));
4350
+ }
4351
+ process.exit(1);
4352
+ }
4353
+
4354
+ const summary = {
4355
+ ok: true,
4356
+ mode,
4357
+ artifact_path: absPath,
4358
+ ...summarizeFeedbackRefinementResult(data),
4359
+ };
4360
+ if (args.json) {
4361
+ console.log(JSON.stringify(summary, null, 2));
4362
+ return;
4363
+ }
4364
+ console.log(`Feedback ${mode}: ${summary.valid ? "valid" : "invalid"}`);
4365
+ console.log(`Feedback: ${summary.feedback_id || feedbackId}`);
4366
+ if (summary.history_id) console.log(`History: ${summary.history_id}`);
4367
+ if (summary.new_snapshot_hash) console.log(`New Snapshot: ${summary.new_snapshot_hash}`);
4368
+ if (summary.current_snapshot_hash) console.log(`Current Snapshot: ${summary.current_snapshot_hash}`);
4369
+ console.log(`Diffs: ${summary.diff_count}`);
4370
+ for (const diff of summary.diffs || []) {
4371
+ console.log(`- ${diff.field}: ${JSON.stringify(diff.from)} -> ${JSON.stringify(diff.to)}`);
4372
+ }
4373
+ if (mode === "apply") {
4374
+ console.log("Local feedback.yml is not modified by apply. Run `myte feedback-sync` to refresh it.");
4375
+ }
4376
+ }
4377
+
4378
+ async function runFeedbackGetOrHistory(args, mode) {
4379
+ const key = getProjectApiKey();
4380
+ if (!key) {
4381
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
4382
+ process.exit(1);
4383
+ }
4384
+ const feedbackId = resolveFeedbackIdArg(args);
4385
+ if (!feedbackId) {
4386
+ console.error("Missing --feedback-id.");
4387
+ process.exit(1);
4388
+ }
4389
+ const timeoutMs = resolveTimeoutMs(args);
4390
+ const apiBase = resolveApiBase(args);
4391
+
4392
+ let data;
4393
+ try {
4394
+ data = mode === "history"
4395
+ ? await fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId })
4396
+ : await fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId });
4397
+ } catch (err) {
4398
+ console.error(`Feedback ${mode} failed:`, err?.message || err);
4399
+ process.exit(1);
4400
+ }
4401
+
4402
+ if (args.json) {
4403
+ console.log(JSON.stringify(data, null, 2));
4404
+ return;
4405
+ }
4406
+ if (mode === "history") {
4407
+ console.log(`Feedback: ${data.feedback_id || feedbackId}`);
4408
+ console.log(`Events: ${data.count || 0}`);
4409
+ for (const event of data.events || []) {
4410
+ console.log(`- ${event.created_at || "unknown"} ${event.action || "refine"} ${event.history_id || ""}`);
4411
+ }
4412
+ return;
4413
+ }
4414
+ const feedback = data.feedback || {};
4415
+ console.log(`Feedback: ${feedback.feedback_id || feedbackId}`);
4416
+ console.log(`Title: ${feedback.title || "(untitled)"}`);
4417
+ console.log(`Status: ${feedback.feedback_state || feedback.status || "unknown"} (${feedback.status || "legacy unknown"})`);
4418
+ console.log(`Priority: ${feedback.priority || "n/a"}`);
4419
+ console.log(`Snapshot: ${feedback.snapshot_hash || "n/a"}`);
4420
+ }
4421
+
4422
+ async function runFeedback(args) {
4423
+ const subcommand = firstNonEmptyString(args._?.[0]) || "help";
4424
+ if (subcommand === "help") {
4425
+ printHelp();
4426
+ return;
4427
+ }
4428
+ if (["status", "edit", "assign", "archive", "refine"].includes(subcommand)) {
4429
+ await buildFeedbackDraftForCommand(args, subcommand);
4430
+ return;
4431
+ }
4432
+ if (subcommand === "validate" || subcommand === "apply") {
4433
+ await runFeedbackValidateOrApply(args, subcommand);
4434
+ return;
4435
+ }
4436
+ if (subcommand === "get" || subcommand === "history") {
4437
+ await runFeedbackGetOrHistory(args, subcommand);
4438
+ return;
4439
+ }
4440
+ console.error("Unknown feedback command. Use status, edit, assign, archive, get, history, validate, or apply.");
4441
+ process.exit(1);
4442
+ }
4443
+
3932
4444
  async function runSuggestionsSync(args) {
3933
4445
  const key = getProjectApiKey();
3934
4446
  if (!key) {
@@ -4670,6 +5182,11 @@ async function main() {
4670
5182
  return;
4671
5183
  }
4672
5184
 
5185
+ if (command === "feedback") {
5186
+ await runFeedback(args);
5187
+ return;
5188
+ }
5189
+
4673
5190
  if (command === "suggestions") {
4674
5191
  await runSuggestions(args);
4675
5192
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "Myte CLI core implementation.",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",