@mytegroupinc/myte-core 0.0.20 → 0.0.22

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 +544 -13
  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
@@ -22,6 +22,7 @@ const {
22
22
  } = require("./lib/ai-gateway");
23
23
 
24
24
  const DEFAULT_API_BASE = "https://api.myte.dev";
25
+ const DEFAULT_DIFF_LIMIT_CHARS = 500_000;
25
26
  const REMOVED_COMMAND_MESSAGES = {
26
27
  ask: "The `ask` alias has been removed. Use `myte query \"...\"`.",
27
28
  chat: "The `chat` command has been removed. Use repeated `myte query` calls instead.",
@@ -98,6 +99,7 @@ function splitCommand(argv) {
98
99
  "update-owner",
99
100
  "update-client",
100
101
  "feedback-sync",
102
+ "feedback",
101
103
  "help",
102
104
  "--help",
103
105
  "-h",
@@ -141,6 +143,17 @@ function parseArgs(argv) {
141
143
  "target-contact-ids",
142
144
  "status",
143
145
  "source",
146
+ "feedback-id",
147
+ "reason",
148
+ "review-action",
149
+ "idempotency-key",
150
+ "user-id",
151
+ "assigned-user-id",
152
+ "due-date",
153
+ "priority",
154
+ "review-note",
155
+ "tags",
156
+ "tag",
144
157
  "mission-ids",
145
158
  "client-session-id",
146
159
  ],
@@ -212,6 +225,10 @@ function printHelp() {
212
225
  " myte update-owner --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--json]",
213
226
  " myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] [--json]",
214
227
  " myte feedback-sync [--status <value>] [--source <value>] [--with-prd-text|--no-with-prd-text] [--output-dir ./MyteCommandCenter] [--json]",
228
+ " myte feedback status --feedback-id <id> --status todo|in_progress|in_review|completed|deployed|rejected|archived --reason \"...\"",
229
+ " myte feedback edit --feedback-id <id> [--title \"...\"] [--feedback-text \"...\"] [--priority High] [--reason \"...\"]",
230
+ " myte feedback validate --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
231
+ " myte feedback apply --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
215
232
  " myte create-prd <file.md> [more.md ...] [--json] [--title \"...\"] [--description \"...\"]",
216
233
  " cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
217
234
  " cat update.md | myte update-owner --stdin --subject \"Owner update\"",
@@ -290,9 +307,15 @@ function printHelp() {
290
307
  " - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
291
308
  " - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
292
309
  "",
310
+ "feedback review contract:",
311
+ " - Draft commands write review artifacts under MyteCommandCenter/reviews/feedback/*.yml for local IDE diff review",
312
+ " - validate/apply send those artifacts to /api/project-assistant/feedback/<id>/refinement/*",
313
+ " - The backend owns authorization, stale snapshot checks, allowed field/transition rules, and history",
314
+ " - apply is idempotent and does not rewrite local feedback.yml; run feedback-sync after apply to refresh local state",
315
+ "",
293
316
  "Options:",
294
317
  " --with-diff Include deterministic git diffs (project-scoped; fails fast if no project repos are configured or resolved)",
295
- " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
318
+ " --diff-limit <chars> Truncate diff context to N chars (default: 500000)",
296
319
  " --timeout-ms <ms> Request timeout (default: 300000)",
297
320
  " --base-url <url> API base (default: https://api.myte.dev)",
298
321
  " --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
@@ -310,8 +333,15 @@ function printHelp() {
310
333
  " --body-file <path> Read update-owner or update-client markdown body from a file",
311
334
  " --target-contact-id Add one client contact ObjectId (repeatable)",
312
335
  " --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).",
336
+ " --feedback-id <id> Feedback ObjectId for feedback review commands",
337
+ " --reason <text> Human review reason included in feedback refinement artifacts",
338
+ " --status <value> Mission status target, feedback-sync filter, or feedback review state depending on command",
314
339
  " --source <value> Feedback source filter for feedback-sync",
340
+ " --priority <value> Feedback review priority target: Low, Medium, or High",
341
+ " --user-id <id> Feedback assignee user id for `myte feedback assign`",
342
+ " --assigned-user-id Alias for --user-id in feedback assign",
343
+ " --due-date <date> Feedback due date target for edit artifacts",
344
+ " --review-note <text> Review note stored in feedback refinement history",
315
345
  " --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
316
346
  " --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
317
347
  " --mission-ids <ids> Comma-separated mission business ids for run-qaqc or mission status (quote multi-id values on PowerShell)",
@@ -340,6 +370,9 @@ function printHelp() {
340
370
  " myte update-owner --subject \"QAQC progress\" --body-file ./updates/owner.md",
341
371
  " myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md",
342
372
  " myte feedback-sync --json",
373
+ " myte feedback status --feedback-id 507f1f77bcf86cd799439011 --status in_review --reason \"Ready for owner review\"",
374
+ " myte feedback validate --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
375
+ " myte feedback apply --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
343
376
  " myte suggestions create --file ./suggestions/create.yml",
344
377
  " myte suggestions revise",
345
378
  " myte suggestions review",
@@ -572,6 +605,8 @@ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
572
605
  } catch {
573
606
  const err = new Error(`Non-JSON response (${resp.status}): ${text.slice(0, 500)}`);
574
607
  err.status = resp.status;
608
+ const retryAfter = resp.headers?.get?.("retry-after");
609
+ if (retryAfter) err.retryAfter = retryAfter;
575
610
  throw err;
576
611
  }
577
612
  return { resp, body };
@@ -1079,21 +1114,35 @@ function resolveConfiguredRepoBindings(bindings) {
1079
1114
 
1080
1115
  const cwd = process.cwd();
1081
1116
  const currentRepoRoot = findGitTopLevel(cwd);
1082
- const scanStart = currentRepoRoot ? path.dirname(currentRepoRoot) : cwd;
1117
+ const searchRoots = [];
1118
+ const seenRoots = new Set();
1119
+ const pushSearchRoot = (value) => {
1120
+ if (!value) return;
1121
+ const resolved = path.resolve(value);
1122
+ if (seenRoots.has(resolved)) return;
1123
+ seenRoots.add(resolved);
1124
+ searchRoots.push(resolved);
1125
+ };
1083
1126
 
1084
- const ancestors = [];
1127
+ // Prefer the invocation workspace first. A wrapper repo can contain the
1128
+ // configured API/WEB repos as children, so jumping immediately to the parent
1129
+ // can miss the project repos entirely.
1130
+ pushSearchRoot(cwd);
1131
+ if (currentRepoRoot) pushSearchRoot(currentRepoRoot);
1132
+
1133
+ const scanStart = currentRepoRoot ? path.dirname(currentRepoRoot) : path.dirname(cwd);
1085
1134
  let cur = scanStart;
1086
1135
  for (let i = 0; i < 8; i += 1) {
1087
- ancestors.push(cur);
1136
+ pushSearchRoot(cur);
1088
1137
  const parent = path.dirname(cur);
1089
1138
  if (parent === cur) break;
1090
1139
  cur = parent;
1091
1140
  }
1092
1141
 
1093
- let fallbackCandidates = currentRepoRoot ? listGitRepoCandidates(path.dirname(currentRepoRoot), currentRepoRoot) : [];
1094
- let fallbackRoot = currentRepoRoot ? path.dirname(currentRepoRoot) : null;
1142
+ let fallbackCandidates = [];
1143
+ let fallbackRoot = null;
1095
1144
 
1096
- for (const candidateRoot of ancestors) {
1145
+ for (const candidateRoot of searchRoots) {
1097
1146
  const candidates = listGitRepoCandidates(candidateRoot, currentRepoRoot);
1098
1147
  if (!fallbackCandidates.length && candidates.length) {
1099
1148
  fallbackCandidates = candidates;
@@ -1585,6 +1634,83 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
1585
1634
  return body.data || {};
1586
1635
  }
1587
1636
 
1637
+ async function fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId }) {
1638
+ const fetchFn = await getFetch();
1639
+ const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}`;
1640
+ const { resp, body } = await fetchJsonWithTimeout(
1641
+ fetchFn,
1642
+ url,
1643
+ {
1644
+ method: "GET",
1645
+ headers: { Authorization: `Bearer ${key}` },
1646
+ },
1647
+ timeoutMs
1648
+ );
1649
+
1650
+ if (!resp.ok || body.status !== "success") {
1651
+ const msg = body?.message || `Feedback review request failed (${resp.status})`;
1652
+ const err = new Error(msg);
1653
+ err.status = resp.status;
1654
+ throw err;
1655
+ }
1656
+ return body.data || {};
1657
+ }
1658
+
1659
+ async function fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId }) {
1660
+ const fetchFn = await getFetch();
1661
+ const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/history`;
1662
+ const { resp, body } = await fetchJsonWithTimeout(
1663
+ fetchFn,
1664
+ url,
1665
+ {
1666
+ method: "GET",
1667
+ headers: { Authorization: `Bearer ${key}` },
1668
+ },
1669
+ timeoutMs
1670
+ );
1671
+
1672
+ if (!resp.ok || body.status !== "success") {
1673
+ const msg = body?.message || `Feedback history request failed (${resp.status})`;
1674
+ const err = new Error(msg);
1675
+ err.status = resp.status;
1676
+ throw err;
1677
+ }
1678
+ return body.data || {};
1679
+ }
1680
+
1681
+ async function postFeedbackRefinement({ apiBase, key, timeoutMs, feedbackId, mode, payload, idempotencyKey, clientSessionId }) {
1682
+ const fetchFn = await getFetch();
1683
+ const action = String(mode || "").trim();
1684
+ const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/${action}`;
1685
+ const headers = {
1686
+ "Content-Type": "application/json",
1687
+ Authorization: `Bearer ${key}`,
1688
+ ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
1689
+ };
1690
+ if (idempotencyKey) {
1691
+ headers["X-Idempotency-Key"] = String(idempotencyKey).trim();
1692
+ }
1693
+ const { resp, body } = await fetchJsonWithTimeout(
1694
+ fetchFn,
1695
+ url,
1696
+ {
1697
+ method: "POST",
1698
+ headers,
1699
+ body: JSON.stringify(payload || {}),
1700
+ },
1701
+ timeoutMs
1702
+ );
1703
+
1704
+ if (!resp.ok || body.status !== "success") {
1705
+ const msg = body?.message || `Feedback refinement ${action} failed (${resp.status})`;
1706
+ const err = new Error(msg);
1707
+ err.status = resp.status;
1708
+ err.data = body?.data;
1709
+ throw err;
1710
+ }
1711
+ return body.data || {};
1712
+ }
1713
+
1588
1714
  async function fetchSuggestionsSyncSnapshot({ apiBase, key, timeoutMs, actorScope = "" }) {
1589
1715
  const fetchFn = await getFetch();
1590
1716
  const url = new URL(`${apiBase}/project-assistant/suggestions`);
@@ -1758,6 +1884,10 @@ function resolveRetryAfterMs(err, fallbackMs = 5_000) {
1758
1884
  return fallbackMs;
1759
1885
  }
1760
1886
 
1887
+ function isTransientQueryStatusError(err) {
1888
+ return [408, 429, 500, 502, 503, 504].includes(Number(err?.status));
1889
+ }
1890
+
1761
1891
  async function createAssistantQueryJob({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
1762
1892
  const fetchFn = await getFetch();
1763
1893
  const url = `${apiBase}${endpoint}`;
@@ -2756,6 +2886,150 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
2756
2886
  };
2757
2887
  }
2758
2888
 
2889
+ function resolveFeedbackReviewPaths(args, { requireFeedback = false } = {}) {
2890
+ const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
2891
+ const targetRoot = outputDir
2892
+ ? path.resolve(process.cwd(), String(outputDir))
2893
+ : findExistingCommandCenterRoot(process.cwd());
2894
+ if (!targetRoot) {
2895
+ if (requireFeedback) {
2896
+ console.error("feedback.yml was not found. Run `myte feedback-sync` first or pass --output-dir.");
2897
+ process.exit(1);
2898
+ }
2899
+ return null;
2900
+ }
2901
+ const dataRoot = path.join(targetRoot, "data");
2902
+ const feedbackPath = path.join(dataRoot, "feedback.yml");
2903
+ if (requireFeedback && !fs.existsSync(feedbackPath)) {
2904
+ console.error("feedback.yml was not found. Run `myte feedback-sync` first or pass --output-dir.");
2905
+ process.exit(1);
2906
+ }
2907
+ return {
2908
+ targetRoot,
2909
+ dataRoot,
2910
+ feedbackPath,
2911
+ reviewsRoot: path.join(targetRoot, "reviews", "feedback"),
2912
+ wrapperRoot: path.dirname(targetRoot),
2913
+ };
2914
+ }
2915
+
2916
+ function readFeedbackManifest(args) {
2917
+ const paths = resolveFeedbackReviewPaths(args, { requireFeedback: true });
2918
+ const manifest = readYamlFile(paths.feedbackPath) || {};
2919
+ if (!isPlainObject(manifest)) {
2920
+ console.error(`Invalid feedback manifest: ${paths.feedbackPath}`);
2921
+ process.exit(1);
2922
+ }
2923
+ return { paths, manifest };
2924
+ }
2925
+
2926
+ function findFeedbackManifestItem(manifest, feedbackId) {
2927
+ const wanted = String(feedbackId || "").trim();
2928
+ if (!wanted) return null;
2929
+ const items = Array.isArray(manifest?.items) ? manifest.items : [];
2930
+ return items.find((item) => String(item?.feedback_id || item?.id || "").trim() === wanted) || null;
2931
+ }
2932
+
2933
+ function readFeedbackRefinementArtifact(args) {
2934
+ const filePath = firstNonEmptyString(args.file);
2935
+ if (!filePath) {
2936
+ console.error("Missing --file for feedback validate/apply.");
2937
+ process.exit(1);
2938
+ }
2939
+ const absPath = resolveInputFile(filePath, "Feedback refinement");
2940
+ const text = fs.readFileSync(absPath, "utf8");
2941
+ let payload;
2942
+ if (absPath.toLowerCase().endsWith(".json")) {
2943
+ payload = JSON.parse(text);
2944
+ } else {
2945
+ payload = parseYaml(text);
2946
+ }
2947
+ if (!isPlainObject(payload)) {
2948
+ console.error(`Feedback refinement file must contain an object: ${absPath}`);
2949
+ process.exit(1);
2950
+ }
2951
+ return { absPath, payload };
2952
+ }
2953
+
2954
+ function parseFeedbackTags(args) {
2955
+ const values = parseCsvValues(args.tags, args.tag);
2956
+ return values.length ? values : undefined;
2957
+ }
2958
+
2959
+ function feedbackChange(from, to) {
2960
+ return { from: from === undefined ? null : from, to: to === undefined ? null : to };
2961
+ }
2962
+
2963
+ function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
2964
+ const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
2965
+ const artifact = {
2966
+ schema_version: 1,
2967
+ kind: "feedback_refinement",
2968
+ project_id: firstNonEmptyString(manifest?.project?.id, item?.project_id),
2969
+ feedback_id: feedbackId,
2970
+ title_snapshot: firstNonEmptyString(item?.title),
2971
+ base_snapshot_hash: firstNonEmptyString(item?.snapshot_hash),
2972
+ base_updated_at: firstNonEmptyString(item?.updated_at),
2973
+ review_action: action,
2974
+ reason: reason || null,
2975
+ changes,
2976
+ force: Boolean(args.force) || undefined,
2977
+ client_session_id: clientSessionId || undefined,
2978
+ created_at: new Date().toISOString(),
2979
+ };
2980
+ return Object.fromEntries(Object.entries(artifact).filter(([, value]) => value !== undefined));
2981
+ }
2982
+
2983
+ function writeFeedbackRefinementArtifact({ paths, artifact, action }) {
2984
+ const feedbackId = sanitizeFileSegment(artifact.feedback_id, "feedback");
2985
+ const actionSegment = sanitizeFileSegment(action || artifact.review_action || "refine", "refine");
2986
+ const filePath = path.join(paths.reviewsRoot, `${feedbackId}-${actionSegment}.yml`);
2987
+ writeYamlFile(filePath, artifact);
2988
+ return filePath;
2989
+ }
2990
+
2991
+ function printFeedbackRefinementDraft({ artifact, filePath, args }) {
2992
+ const output = {
2993
+ feedback_id: artifact.feedback_id,
2994
+ review_action: artifact.review_action,
2995
+ artifact_path: filePath || null,
2996
+ base_snapshot_hash: artifact.base_snapshot_hash || null,
2997
+ changes: artifact.changes || {},
2998
+ reason: artifact.reason || null,
2999
+ };
3000
+ if (args.json) {
3001
+ console.log(JSON.stringify(output, null, 2));
3002
+ return;
3003
+ }
3004
+ console.log(`Feedback: ${output.feedback_id}`);
3005
+ console.log(`Action: ${output.review_action}`);
3006
+ if (filePath) console.log(`Artifact: ${filePath}`);
3007
+ console.log(`Base Snapshot: ${output.base_snapshot_hash || "n/a"}`);
3008
+ console.log("Changes:");
3009
+ for (const [field, change] of Object.entries(output.changes || {})) {
3010
+ const fromValue = change && typeof change === "object" && "from" in change ? change.from : null;
3011
+ const toValue = change && typeof change === "object" && "to" in change ? change.to : change;
3012
+ console.log(`- ${field}: ${JSON.stringify(fromValue)} -> ${JSON.stringify(toValue)}`);
3013
+ }
3014
+ if (output.reason) console.log(`Reason: ${output.reason}`);
3015
+ }
3016
+
3017
+ function summarizeFeedbackRefinementResult(data) {
3018
+ return {
3019
+ valid: Boolean(data?.valid),
3020
+ feedback_id: data?.feedback_id || data?.feedback?.feedback_id || null,
3021
+ project_id: data?.project_id || null,
3022
+ previous_snapshot_hash: data?.previous_snapshot_hash || data?.base_snapshot_hash || null,
3023
+ current_snapshot_hash: data?.current_snapshot_hash || null,
3024
+ new_snapshot_hash: data?.new_snapshot_hash || data?.feedback?.snapshot_hash || null,
3025
+ diff_count: Array.isArray(data?.diffs) ? data.diffs.length : 0,
3026
+ diffs: data?.diffs || [],
3027
+ warnings: data?.warnings || [],
3028
+ errors: data?.errors || [],
3029
+ history_id: data?.history?.history_id || null,
3030
+ };
3031
+ }
3032
+
2759
3033
  function missionOpsThreads(payload) {
2760
3034
  if (Array.isArray(payload?.threads)) return payload.threads;
2761
3035
  if (Array.isArray(payload?.suggestions)) return payload.suggestions;
@@ -3929,6 +4203,251 @@ async function runFeedbackSync(args) {
3929
4203
  console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
3930
4204
  }
3931
4205
 
4206
+ function resolveFeedbackIdArg(args) {
4207
+ return firstNonEmptyString(args["feedback-id"], args.feedbackId, args.feedback_id, args._?.[1]);
4208
+ }
4209
+
4210
+ function requireFeedbackDraftContext(args) {
4211
+ const feedbackId = resolveFeedbackIdArg(args);
4212
+ if (!feedbackId) {
4213
+ console.error("Missing --feedback-id.");
4214
+ process.exit(1);
4215
+ }
4216
+ const { paths, manifest } = readFeedbackManifest(args);
4217
+ const item = findFeedbackManifestItem(manifest, feedbackId);
4218
+ if (!item) {
4219
+ console.error(`Feedback item not found in feedback.yml: ${feedbackId}. Run \`myte feedback-sync\` first.`);
4220
+ process.exit(1);
4221
+ }
4222
+ return { paths, manifest, item, feedbackId };
4223
+ }
4224
+
4225
+ async function buildFeedbackDraftForCommand(args, subcommand) {
4226
+ const { paths, manifest, item, feedbackId } = requireFeedbackDraftContext(args);
4227
+ const action = subcommand || "refine";
4228
+ const reason = firstNonEmptyString(args.reason);
4229
+ const changes = {};
4230
+
4231
+ if (action === "status") {
4232
+ const targetStatus = firstNonEmptyString(args.status, args._?.[2]);
4233
+ if (!targetStatus) {
4234
+ console.error("Missing --status for feedback status.");
4235
+ process.exit(1);
4236
+ }
4237
+ changes.feedback_state = feedbackChange(firstNonEmptyString(item.feedback_state, item.status), targetStatus);
4238
+ } else if (action === "archive") {
4239
+ changes.feedback_state = feedbackChange(firstNonEmptyString(item.feedback_state, item.status), "archived");
4240
+ } else if (action === "assign") {
4241
+ const assignee = firstNonEmptyString(args["assigned-user-id"], args.assignedUserId, args.assigned_user_id, args["user-id"], args.userId, args.user_id, args._?.[2]);
4242
+ if (!assignee) {
4243
+ console.error("Missing --user-id or --assigned-user-id for feedback assign.");
4244
+ process.exit(1);
4245
+ }
4246
+ changes.assigned_user_id = feedbackChange(firstNonEmptyString(item.assigned_user_id), assignee);
4247
+ } else if (action === "edit" || action === "refine") {
4248
+ const title = firstNonEmptyString(args.title);
4249
+ if (title) changes.title = feedbackChange(firstNonEmptyString(item.title), title);
4250
+
4251
+ let feedbackText = firstNonEmptyString(args["feedback-text"], args.feedbackText, args.feedback_text);
4252
+ const bodyFile = firstNonEmptyString(args["body-file"], args.bodyFile, args.body_file);
4253
+ if (!feedbackText && bodyFile) {
4254
+ const bodyPath = resolveInputFile(bodyFile, "Feedback body");
4255
+ feedbackText = fs.readFileSync(bodyPath, "utf8").trim();
4256
+ }
4257
+ if (feedbackText) changes.feedback_text = feedbackChange(firstNonEmptyString(item.feedback_text), feedbackText);
4258
+
4259
+ const priority = firstNonEmptyString(args.priority);
4260
+ if (priority) changes.priority = feedbackChange(firstNonEmptyString(item.priority), priority);
4261
+
4262
+ const dueDate = firstNonEmptyString(args["due-date"], args.dueDate, args.due_date);
4263
+ if (dueDate) changes.due_date = feedbackChange(firstNonEmptyString(item.due_date, item.due_date_text), dueDate);
4264
+
4265
+ const tags = parseFeedbackTags(args);
4266
+ if (tags) changes.tags = feedbackChange(Array.isArray(item.tags) ? item.tags : [], tags);
4267
+
4268
+ const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
4269
+ if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
4270
+ } else {
4271
+ console.error("Unknown feedback command. Use status, edit, assign, archive, get, history, validate, or apply.");
4272
+ process.exit(1);
4273
+ }
4274
+
4275
+ if (!Object.keys(changes).length) {
4276
+ console.error("No feedback changes were provided.");
4277
+ process.exit(1);
4278
+ }
4279
+
4280
+ const artifact = buildFeedbackRefinementArtifact({
4281
+ manifest,
4282
+ item,
4283
+ feedbackId,
4284
+ action,
4285
+ changes,
4286
+ reason,
4287
+ args,
4288
+ });
4289
+
4290
+ if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
4291
+ if (args.json) {
4292
+ console.log(JSON.stringify(artifact, null, 2));
4293
+ } else {
4294
+ console.log(stringifyYaml(artifact));
4295
+ }
4296
+ return;
4297
+ }
4298
+
4299
+ const filePath = writeFeedbackRefinementArtifact({ paths, artifact, action });
4300
+ printFeedbackRefinementDraft({ artifact, filePath, args });
4301
+ }
4302
+
4303
+ async function runFeedbackValidateOrApply(args, mode) {
4304
+ const key = getProjectApiKey();
4305
+ if (!key) {
4306
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
4307
+ process.exit(1);
4308
+ }
4309
+ const { absPath, payload } = readFeedbackRefinementArtifact(args);
4310
+ const feedbackId = firstNonEmptyString(args["feedback-id"], args.feedbackId, args.feedback_id, payload.feedback_id);
4311
+ if (!feedbackId) {
4312
+ console.error("Feedback refinement artifact is missing feedback_id.");
4313
+ process.exit(1);
4314
+ }
4315
+ if (args.force) payload.force = true;
4316
+
4317
+ const timeoutMs = resolveTimeoutMs(args);
4318
+ const apiBase = resolveApiBase(args);
4319
+ const clientSessionId = firstNonEmptyString(
4320
+ args["client-session-id"],
4321
+ args.clientSessionId,
4322
+ args.client_session_id,
4323
+ payload.client_session_id
4324
+ );
4325
+ const idempotencyKey = mode === "apply"
4326
+ ? resolveProjectMutationIdempotencyKey({
4327
+ args,
4328
+ operation: `feedback_refinement_apply:${feedbackId}`,
4329
+ payload,
4330
+ })
4331
+ : null;
4332
+
4333
+ let data;
4334
+ try {
4335
+ data = await postFeedbackRefinement({
4336
+ apiBase,
4337
+ key,
4338
+ timeoutMs,
4339
+ feedbackId,
4340
+ mode,
4341
+ payload,
4342
+ idempotencyKey,
4343
+ clientSessionId,
4344
+ });
4345
+ } catch (err) {
4346
+ if (args.json) {
4347
+ console.log(JSON.stringify({
4348
+ ok: false,
4349
+ status: err?.status || null,
4350
+ message: err?.message || String(err),
4351
+ data: err?.data || null,
4352
+ artifact_path: absPath,
4353
+ }, null, 2));
4354
+ } else {
4355
+ console.error(`Feedback ${mode} failed:`, err?.message || err);
4356
+ if (err?.data) console.error(JSON.stringify(err.data, null, 2));
4357
+ }
4358
+ process.exit(1);
4359
+ }
4360
+
4361
+ const summary = {
4362
+ ok: true,
4363
+ mode,
4364
+ artifact_path: absPath,
4365
+ ...summarizeFeedbackRefinementResult(data),
4366
+ };
4367
+ if (args.json) {
4368
+ console.log(JSON.stringify(summary, null, 2));
4369
+ return;
4370
+ }
4371
+ console.log(`Feedback ${mode}: ${summary.valid ? "valid" : "invalid"}`);
4372
+ console.log(`Feedback: ${summary.feedback_id || feedbackId}`);
4373
+ if (summary.history_id) console.log(`History: ${summary.history_id}`);
4374
+ if (summary.new_snapshot_hash) console.log(`New Snapshot: ${summary.new_snapshot_hash}`);
4375
+ if (summary.current_snapshot_hash) console.log(`Current Snapshot: ${summary.current_snapshot_hash}`);
4376
+ console.log(`Diffs: ${summary.diff_count}`);
4377
+ for (const diff of summary.diffs || []) {
4378
+ console.log(`- ${diff.field}: ${JSON.stringify(diff.from)} -> ${JSON.stringify(diff.to)}`);
4379
+ }
4380
+ if (mode === "apply") {
4381
+ console.log("Local feedback.yml is not modified by apply. Run `myte feedback-sync` to refresh it.");
4382
+ }
4383
+ }
4384
+
4385
+ async function runFeedbackGetOrHistory(args, mode) {
4386
+ const key = getProjectApiKey();
4387
+ if (!key) {
4388
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
4389
+ process.exit(1);
4390
+ }
4391
+ const feedbackId = resolveFeedbackIdArg(args);
4392
+ if (!feedbackId) {
4393
+ console.error("Missing --feedback-id.");
4394
+ process.exit(1);
4395
+ }
4396
+ const timeoutMs = resolveTimeoutMs(args);
4397
+ const apiBase = resolveApiBase(args);
4398
+
4399
+ let data;
4400
+ try {
4401
+ data = mode === "history"
4402
+ ? await fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId })
4403
+ : await fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId });
4404
+ } catch (err) {
4405
+ console.error(`Feedback ${mode} failed:`, err?.message || err);
4406
+ process.exit(1);
4407
+ }
4408
+
4409
+ if (args.json) {
4410
+ console.log(JSON.stringify(data, null, 2));
4411
+ return;
4412
+ }
4413
+ if (mode === "history") {
4414
+ console.log(`Feedback: ${data.feedback_id || feedbackId}`);
4415
+ console.log(`Events: ${data.count || 0}`);
4416
+ for (const event of data.events || []) {
4417
+ console.log(`- ${event.created_at || "unknown"} ${event.action || "refine"} ${event.history_id || ""}`);
4418
+ }
4419
+ return;
4420
+ }
4421
+ const feedback = data.feedback || {};
4422
+ console.log(`Feedback: ${feedback.feedback_id || feedbackId}`);
4423
+ console.log(`Title: ${feedback.title || "(untitled)"}`);
4424
+ console.log(`Status: ${feedback.feedback_state || feedback.status || "unknown"} (${feedback.status || "legacy unknown"})`);
4425
+ console.log(`Priority: ${feedback.priority || "n/a"}`);
4426
+ console.log(`Snapshot: ${feedback.snapshot_hash || "n/a"}`);
4427
+ }
4428
+
4429
+ async function runFeedback(args) {
4430
+ const subcommand = firstNonEmptyString(args._?.[0]) || "help";
4431
+ if (subcommand === "help") {
4432
+ printHelp();
4433
+ return;
4434
+ }
4435
+ if (["status", "edit", "assign", "archive", "refine"].includes(subcommand)) {
4436
+ await buildFeedbackDraftForCommand(args, subcommand);
4437
+ return;
4438
+ }
4439
+ if (subcommand === "validate" || subcommand === "apply") {
4440
+ await runFeedbackValidateOrApply(args, subcommand);
4441
+ return;
4442
+ }
4443
+ if (subcommand === "get" || subcommand === "history") {
4444
+ await runFeedbackGetOrHistory(args, subcommand);
4445
+ return;
4446
+ }
4447
+ console.error("Unknown feedback command. Use status, edit, assign, archive, get, history, validate, or apply.");
4448
+ process.exit(1);
4449
+ }
4450
+
3932
4451
  async function runSuggestionsSync(args) {
3933
4452
  const key = getProjectApiKey();
3934
4453
  if (!key) {
@@ -4490,8 +5009,9 @@ async function runQuery(args) {
4490
5009
  const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
4491
5010
 
4492
5011
  const charLimitRaw = args["diff-limit"] || args.diffLimit || args.diff_limit;
4493
- const charLimitParsed = charLimitRaw !== undefined && charLimitRaw !== null ? Number(charLimitRaw) : 200_000;
4494
- const diffLimit = Number.isFinite(charLimitParsed) ? charLimitParsed : 200_000;
5012
+ const charLimitParsed =
5013
+ charLimitRaw !== undefined && charLimitRaw !== null ? Number(charLimitRaw) : DEFAULT_DIFF_LIMIT_CHARS;
5014
+ const diffLimit = Number.isFinite(charLimitParsed) ? charLimitParsed : DEFAULT_DIFF_LIMIT_CHARS;
4495
5015
 
4496
5016
  const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
4497
5017
  const apiBase = normalizeApiBase(baseRaw);
@@ -4565,13 +5085,19 @@ async function runQuery(args) {
4565
5085
  const pollTimeoutMs = Math.max(timeoutMs, 900_000);
4566
5086
  const startedAt = Date.now();
4567
5087
  let finalStatus = null;
5088
+ let pollDelayMs = 2_000;
4568
5089
  do {
4569
- await sleep(2_000);
5090
+ await sleep(pollDelayMs);
4570
5091
  try {
4571
5092
  finalStatus = await fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId });
5093
+ pollDelayMs = Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
4572
5094
  } catch (err) {
4573
- if (Number(err?.status) === 429) {
4574
- await sleep(resolveRetryAfterMs(err));
5095
+ if (isTransientQueryStatusError(err)) {
5096
+ const fallbackMs =
5097
+ Number(err?.status) === 429 ? 5_000 : Math.min(30_000, Math.max(5_000, pollDelayMs * 2));
5098
+ const waitMs = resolveRetryAfterMs(err, fallbackMs);
5099
+ await sleep(waitMs);
5100
+ pollDelayMs = Math.min(30_000, Math.max(pollDelayMs, waitMs));
4575
5101
  continue;
4576
5102
  }
4577
5103
  throw err;
@@ -4670,6 +5196,11 @@ async function main() {
4670
5196
  return;
4671
5197
  }
4672
5198
 
5199
+ if (command === "feedback") {
5200
+ await runFeedback(args);
5201
+ return;
5202
+ }
5203
+
4673
5204
  if (command === "suggestions") {
4674
5205
  await runSuggestions(args);
4675
5206
  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.22",
4
4
  "description": "Myte CLI core implementation.",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",